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        let previous_edit_state = self.edit_state.take();
1350        self.update_visible_entries(None, cx);
1351        self.marked_entries.clear();
1352
1353        if let Some(previously_focused) =
1354            previous_edit_state.and_then(|edit_state| edit_state.previously_focused)
1355        {
1356            self.selection = Some(previously_focused);
1357            self.autoscroll(cx);
1358        }
1359
1360        window.focus(&self.focus_handle);
1361        cx.notify();
1362    }
1363
1364    fn open_entry(
1365        &mut self,
1366        entry_id: ProjectEntryId,
1367        focus_opened_item: bool,
1368        allow_preview: bool,
1369
1370        cx: &mut Context<Self>,
1371    ) {
1372        cx.emit(Event::OpenedEntry {
1373            entry_id,
1374            focus_opened_item,
1375            allow_preview,
1376        });
1377    }
1378
1379    fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut Context<Self>) {
1380        cx.emit(Event::SplitEntry { entry_id });
1381    }
1382
1383    fn new_file(&mut self, _: &NewFile, window: &mut Window, cx: &mut Context<Self>) {
1384        self.add_entry(false, window, cx)
1385    }
1386
1387    fn new_directory(&mut self, _: &NewDirectory, window: &mut Window, cx: &mut Context<Self>) {
1388        self.add_entry(true, window, cx)
1389    }
1390
1391    fn add_entry(&mut self, is_dir: bool, window: &mut Window, cx: &mut Context<Self>) {
1392        let Some((worktree_id, entry_id)) = self
1393            .selection
1394            .map(|entry| (entry.worktree_id, entry.entry_id))
1395            .or_else(|| {
1396                let entry_id = self.last_worktree_root_id?;
1397                let worktree_id = self
1398                    .project
1399                    .read(cx)
1400                    .worktree_for_entry(entry_id, cx)?
1401                    .read(cx)
1402                    .id();
1403
1404                self.selection = Some(SelectedEntry {
1405                    worktree_id,
1406                    entry_id,
1407                });
1408
1409                Some((worktree_id, entry_id))
1410            })
1411        else {
1412            return;
1413        };
1414
1415        let directory_id;
1416        let new_entry_id = self.resolve_entry(entry_id);
1417        if let Some((worktree, expanded_dir_ids)) = self
1418            .project
1419            .read(cx)
1420            .worktree_for_id(worktree_id, cx)
1421            .zip(self.expanded_dir_ids.get_mut(&worktree_id))
1422        {
1423            let worktree = worktree.read(cx);
1424            if let Some(mut entry) = worktree.entry_for_id(new_entry_id) {
1425                loop {
1426                    if entry.is_dir() {
1427                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1428                            expanded_dir_ids.insert(ix, entry.id);
1429                        }
1430                        directory_id = entry.id;
1431                        break;
1432                    } else {
1433                        if let Some(parent_path) = entry.path.parent() {
1434                            if let Some(parent_entry) = worktree.entry_for_path(parent_path) {
1435                                entry = parent_entry;
1436                                continue;
1437                            }
1438                        }
1439                        return;
1440                    }
1441                }
1442            } else {
1443                return;
1444            };
1445        } else {
1446            return;
1447        };
1448
1449        self.marked_entries.clear();
1450        self.edit_state = Some(EditState {
1451            worktree_id,
1452            entry_id: directory_id,
1453            leaf_entry_id: None,
1454            is_dir,
1455            processing_filename: None,
1456            previously_focused: self.selection,
1457            depth: 0,
1458            validation_state: ValidationState::None,
1459        });
1460        self.filename_editor.update(cx, |editor, cx| {
1461            editor.clear(window, cx);
1462            window.focus(&editor.focus_handle(cx));
1463        });
1464        self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx);
1465        self.autoscroll(cx);
1466        cx.notify();
1467    }
1468
1469    fn unflatten_entry_id(&self, leaf_entry_id: ProjectEntryId) -> ProjectEntryId {
1470        if let Some(ancestors) = self.ancestors.get(&leaf_entry_id) {
1471            ancestors
1472                .ancestors
1473                .get(ancestors.current_ancestor_depth)
1474                .copied()
1475                .unwrap_or(leaf_entry_id)
1476        } else {
1477            leaf_entry_id
1478        }
1479    }
1480
1481    fn rename_impl(
1482        &mut self,
1483        selection: Option<Range<usize>>,
1484        window: &mut Window,
1485        cx: &mut Context<Self>,
1486    ) {
1487        if let Some(SelectedEntry {
1488            worktree_id,
1489            entry_id,
1490        }) = self.selection
1491        {
1492            if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) {
1493                let sub_entry_id = self.unflatten_entry_id(entry_id);
1494                if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) {
1495                    #[cfg(target_os = "windows")]
1496                    if Some(entry) == worktree.read(cx).root_entry() {
1497                        return;
1498                    }
1499                    self.edit_state = Some(EditState {
1500                        worktree_id,
1501                        entry_id: sub_entry_id,
1502                        leaf_entry_id: Some(entry_id),
1503                        is_dir: entry.is_dir(),
1504                        processing_filename: None,
1505                        previously_focused: None,
1506                        depth: 0,
1507                        validation_state: ValidationState::None,
1508                    });
1509                    let file_name = entry
1510                        .path
1511                        .file_name()
1512                        .map(|s| s.to_string_lossy())
1513                        .unwrap_or_default()
1514                        .to_string();
1515                    let selection = selection.unwrap_or_else(|| {
1516                        let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
1517                        let selection_end =
1518                            file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
1519                        0..selection_end
1520                    });
1521                    self.filename_editor.update(cx, |editor, cx| {
1522                        editor.set_text(file_name, window, cx);
1523                        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1524                            s.select_ranges([selection])
1525                        });
1526                        window.focus(&editor.focus_handle(cx));
1527                    });
1528                    self.update_visible_entries(None, cx);
1529                    self.autoscroll(cx);
1530                    cx.notify();
1531                }
1532            }
1533        }
1534    }
1535
1536    fn rename(&mut self, _: &Rename, window: &mut Window, cx: &mut Context<Self>) {
1537        self.rename_impl(None, window, cx);
1538    }
1539
1540    fn trash(&mut self, action: &Trash, window: &mut Window, cx: &mut Context<Self>) {
1541        self.remove(true, action.skip_prompt, window, cx);
1542    }
1543
1544    fn delete(&mut self, action: &Delete, window: &mut Window, cx: &mut Context<Self>) {
1545        self.remove(false, action.skip_prompt, window, cx);
1546    }
1547
1548    fn remove(
1549        &mut self,
1550        trash: bool,
1551        skip_prompt: bool,
1552        window: &mut Window,
1553        cx: &mut Context<ProjectPanel>,
1554    ) {
1555        maybe!({
1556            let items_to_delete = self.disjoint_entries(cx);
1557            if items_to_delete.is_empty() {
1558                return None;
1559            }
1560            let project = self.project.read(cx);
1561
1562            let mut dirty_buffers = 0;
1563            let file_paths = items_to_delete
1564                .iter()
1565                .filter_map(|selection| {
1566                    let project_path = project.path_for_entry(selection.entry_id, cx)?;
1567                    dirty_buffers +=
1568                        project.dirty_buffers(cx).any(|path| path == project_path) as usize;
1569                    Some((
1570                        selection.entry_id,
1571                        project_path
1572                            .path
1573                            .file_name()?
1574                            .to_string_lossy()
1575                            .into_owned(),
1576                    ))
1577                })
1578                .collect::<Vec<_>>();
1579            if file_paths.is_empty() {
1580                return None;
1581            }
1582            let answer = if !skip_prompt {
1583                let operation = if trash { "Trash" } else { "Delete" };
1584                let prompt = match file_paths.first() {
1585                    Some((_, path)) if file_paths.len() == 1 => {
1586                        let unsaved_warning = if dirty_buffers > 0 {
1587                            "\n\nIt has unsaved changes, which will be lost."
1588                        } else {
1589                            ""
1590                        };
1591
1592                        format!("{operation} {path}?{unsaved_warning}")
1593                    }
1594                    _ => {
1595                        const CUTOFF_POINT: usize = 10;
1596                        let names = if file_paths.len() > CUTOFF_POINT {
1597                            let truncated_path_counts = file_paths.len() - CUTOFF_POINT;
1598                            let mut paths = file_paths
1599                                .iter()
1600                                .map(|(_, path)| path.clone())
1601                                .take(CUTOFF_POINT)
1602                                .collect::<Vec<_>>();
1603                            paths.truncate(CUTOFF_POINT);
1604                            if truncated_path_counts == 1 {
1605                                paths.push(".. 1 file not shown".into());
1606                            } else {
1607                                paths.push(format!(".. {} files not shown", truncated_path_counts));
1608                            }
1609                            paths
1610                        } else {
1611                            file_paths.iter().map(|(_, path)| path.clone()).collect()
1612                        };
1613                        let unsaved_warning = if dirty_buffers == 0 {
1614                            String::new()
1615                        } else if dirty_buffers == 1 {
1616                            "\n\n1 of these has unsaved changes, which will be lost.".to_string()
1617                        } else {
1618                            format!(
1619                                "\n\n{dirty_buffers} of these have unsaved changes, which will be lost."
1620                            )
1621                        };
1622
1623                        format!(
1624                            "Do you want to {} the following {} files?\n{}{unsaved_warning}",
1625                            operation.to_lowercase(),
1626                            file_paths.len(),
1627                            names.join("\n")
1628                        )
1629                    }
1630                };
1631                Some(window.prompt(PromptLevel::Info, &prompt, None, &[operation, "Cancel"], cx))
1632            } else {
1633                None
1634            };
1635            let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx);
1636            cx.spawn_in(window, async move |panel, cx| {
1637                if let Some(answer) = answer {
1638                    if answer.await != Ok(0) {
1639                        return anyhow::Ok(());
1640                    }
1641                }
1642                for (entry_id, _) in file_paths {
1643                    panel
1644                        .update(cx, |panel, cx| {
1645                            panel
1646                                .project
1647                                .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
1648                                .context("no such entry")
1649                        })??
1650                        .await?;
1651                }
1652                panel.update_in(cx, |panel, window, cx| {
1653                    if let Some(next_selection) = next_selection {
1654                        panel.selection = Some(next_selection);
1655                        panel.autoscroll(cx);
1656                    } else {
1657                        panel.select_last(&SelectLast {}, window, cx);
1658                    }
1659                })?;
1660                Ok(())
1661            })
1662            .detach_and_log_err(cx);
1663            Some(())
1664        });
1665    }
1666
1667    fn find_next_selection_after_deletion(
1668        &self,
1669        sanitized_entries: BTreeSet<SelectedEntry>,
1670        cx: &mut Context<Self>,
1671    ) -> Option<SelectedEntry> {
1672        if sanitized_entries.is_empty() {
1673            return None;
1674        }
1675        let project = self.project.read(cx);
1676        let (worktree_id, worktree) = sanitized_entries
1677            .iter()
1678            .map(|entry| entry.worktree_id)
1679            .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
1680            .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
1681        let git_store = project.git_store().read(cx);
1682
1683        let marked_entries_in_worktree = sanitized_entries
1684            .iter()
1685            .filter(|e| e.worktree_id == worktree_id)
1686            .collect::<HashSet<_>>();
1687        let latest_entry = marked_entries_in_worktree
1688            .iter()
1689            .max_by(|a, b| {
1690                match (
1691                    worktree.entry_for_id(a.entry_id),
1692                    worktree.entry_for_id(b.entry_id),
1693                ) {
1694                    (Some(a), Some(b)) => {
1695                        compare_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
1696                    }
1697                    _ => cmp::Ordering::Equal,
1698                }
1699            })
1700            .and_then(|e| worktree.entry_for_id(e.entry_id))?;
1701
1702        let parent_path = latest_entry.path.parent()?;
1703        let parent_entry = worktree.entry_for_path(parent_path)?;
1704
1705        // Remove all siblings that are being deleted except the last marked entry
1706        let repo_snapshots = git_store.repo_snapshots(cx);
1707        let worktree_snapshot = worktree.snapshot();
1708        let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
1709        let mut siblings: Vec<_> =
1710            ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path)
1711                .filter(|sibling| {
1712                    (sibling.id == latest_entry.id)
1713                        || (!marked_entries_in_worktree.contains(&&SelectedEntry {
1714                            worktree_id,
1715                            entry_id: sibling.id,
1716                        }) && (!hide_gitignore || !sibling.is_ignored))
1717                })
1718                .map(|entry| entry.to_owned())
1719                .collect();
1720
1721        project::sort_worktree_entries(&mut siblings);
1722        let sibling_entry_index = siblings
1723            .iter()
1724            .position(|sibling| sibling.id == latest_entry.id)?;
1725
1726        if let Some(next_sibling) = sibling_entry_index
1727            .checked_add(1)
1728            .and_then(|i| siblings.get(i))
1729        {
1730            return Some(SelectedEntry {
1731                worktree_id,
1732                entry_id: next_sibling.id,
1733            });
1734        }
1735        if let Some(prev_sibling) = sibling_entry_index
1736            .checked_sub(1)
1737            .and_then(|i| siblings.get(i))
1738        {
1739            return Some(SelectedEntry {
1740                worktree_id,
1741                entry_id: prev_sibling.id,
1742            });
1743        }
1744        // No neighbour sibling found, fall back to parent
1745        Some(SelectedEntry {
1746            worktree_id,
1747            entry_id: parent_entry.id,
1748        })
1749    }
1750
1751    fn unfold_directory(&mut self, _: &UnfoldDirectory, _: &mut Window, cx: &mut Context<Self>) {
1752        if let Some((worktree, entry)) = self.selected_entry(cx) {
1753            self.unfolded_dir_ids.insert(entry.id);
1754
1755            let snapshot = worktree.snapshot();
1756            let mut parent_path = entry.path.parent();
1757            while let Some(path) = parent_path {
1758                if let Some(parent_entry) = worktree.entry_for_path(path) {
1759                    let mut children_iter = snapshot.child_entries(path);
1760
1761                    if children_iter.by_ref().take(2).count() > 1 {
1762                        break;
1763                    }
1764
1765                    self.unfolded_dir_ids.insert(parent_entry.id);
1766                    parent_path = path.parent();
1767                } else {
1768                    break;
1769                }
1770            }
1771
1772            self.update_visible_entries(None, cx);
1773            self.autoscroll(cx);
1774            cx.notify();
1775        }
1776    }
1777
1778    fn fold_directory(&mut self, _: &FoldDirectory, _: &mut Window, cx: &mut Context<Self>) {
1779        if let Some((worktree, entry)) = self.selected_entry(cx) {
1780            self.unfolded_dir_ids.remove(&entry.id);
1781
1782            let snapshot = worktree.snapshot();
1783            let mut path = &*entry.path;
1784            loop {
1785                let mut child_entries_iter = snapshot.child_entries(path);
1786                if let Some(child) = child_entries_iter.next() {
1787                    if child_entries_iter.next().is_none() && child.is_dir() {
1788                        self.unfolded_dir_ids.remove(&child.id);
1789                        path = &*child.path;
1790                    } else {
1791                        break;
1792                    }
1793                } else {
1794                    break;
1795                }
1796            }
1797
1798            self.update_visible_entries(None, cx);
1799            self.autoscroll(cx);
1800            cx.notify();
1801        }
1802    }
1803
1804    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1805        if let Some(edit_state) = &self.edit_state {
1806            if edit_state.processing_filename.is_none() {
1807                self.filename_editor.update(cx, |editor, cx| {
1808                    editor.move_to_end_of_line(
1809                        &editor::actions::MoveToEndOfLine {
1810                            stop_at_soft_wraps: false,
1811                        },
1812                        window,
1813                        cx,
1814                    );
1815                });
1816                return;
1817            }
1818        }
1819        if let Some(selection) = self.selection {
1820            let (mut worktree_ix, mut entry_ix, _) =
1821                self.index_for_selection(selection).unwrap_or_default();
1822            if let Some((_, worktree_entries, _)) = self.visible_entries.get(worktree_ix) {
1823                if entry_ix + 1 < worktree_entries.len() {
1824                    entry_ix += 1;
1825                } else {
1826                    worktree_ix += 1;
1827                    entry_ix = 0;
1828                }
1829            }
1830
1831            if let Some((worktree_id, worktree_entries, _)) = self.visible_entries.get(worktree_ix)
1832            {
1833                if let Some(entry) = worktree_entries.get(entry_ix) {
1834                    let selection = SelectedEntry {
1835                        worktree_id: *worktree_id,
1836                        entry_id: entry.id,
1837                    };
1838                    self.selection = Some(selection);
1839                    if window.modifiers().shift {
1840                        self.marked_entries.insert(selection);
1841                    }
1842
1843                    self.autoscroll(cx);
1844                    cx.notify();
1845                }
1846            }
1847        } else {
1848            self.select_first(&SelectFirst {}, window, cx);
1849        }
1850    }
1851
1852    fn select_prev_diagnostic(
1853        &mut self,
1854        _: &SelectPrevDiagnostic,
1855        _: &mut Window,
1856        cx: &mut Context<Self>,
1857    ) {
1858        let selection = self.find_entry(
1859            self.selection.as_ref(),
1860            true,
1861            |entry, worktree_id| {
1862                (self.selection.is_none()
1863                    || self.selection.is_some_and(|selection| {
1864                        if selection.worktree_id == worktree_id {
1865                            selection.entry_id != entry.id
1866                        } else {
1867                            true
1868                        }
1869                    }))
1870                    && entry.is_file()
1871                    && self
1872                        .diagnostics
1873                        .contains_key(&(worktree_id, entry.path.to_path_buf()))
1874            },
1875            cx,
1876        );
1877
1878        if let Some(selection) = selection {
1879            self.selection = Some(selection);
1880            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1881            self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1882            self.autoscroll(cx);
1883            cx.notify();
1884        }
1885    }
1886
1887    fn select_next_diagnostic(
1888        &mut self,
1889        _: &SelectNextDiagnostic,
1890        _: &mut Window,
1891        cx: &mut Context<Self>,
1892    ) {
1893        let selection = self.find_entry(
1894            self.selection.as_ref(),
1895            false,
1896            |entry, worktree_id| {
1897                (self.selection.is_none()
1898                    || self.selection.is_some_and(|selection| {
1899                        if selection.worktree_id == worktree_id {
1900                            selection.entry_id != entry.id
1901                        } else {
1902                            true
1903                        }
1904                    }))
1905                    && entry.is_file()
1906                    && self
1907                        .diagnostics
1908                        .contains_key(&(worktree_id, entry.path.to_path_buf()))
1909            },
1910            cx,
1911        );
1912
1913        if let Some(selection) = selection {
1914            self.selection = Some(selection);
1915            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1916            self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1917            self.autoscroll(cx);
1918            cx.notify();
1919        }
1920    }
1921
1922    fn select_prev_git_entry(
1923        &mut self,
1924        _: &SelectPrevGitEntry,
1925        _: &mut Window,
1926        cx: &mut Context<Self>,
1927    ) {
1928        let selection = self.find_entry(
1929            self.selection.as_ref(),
1930            true,
1931            |entry, worktree_id| {
1932                (self.selection.is_none()
1933                    || self.selection.is_some_and(|selection| {
1934                        if selection.worktree_id == worktree_id {
1935                            selection.entry_id != entry.id
1936                        } else {
1937                            true
1938                        }
1939                    }))
1940                    && entry.is_file()
1941                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
1942            },
1943            cx,
1944        );
1945
1946        if let Some(selection) = selection {
1947            self.selection = Some(selection);
1948            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1949            self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1950            self.autoscroll(cx);
1951            cx.notify();
1952        }
1953    }
1954
1955    fn select_prev_directory(
1956        &mut self,
1957        _: &SelectPrevDirectory,
1958        _: &mut Window,
1959        cx: &mut Context<Self>,
1960    ) {
1961        let selection = self.find_visible_entry(
1962            self.selection.as_ref(),
1963            true,
1964            |entry, worktree_id| {
1965                (self.selection.is_none()
1966                    || self.selection.is_some_and(|selection| {
1967                        if selection.worktree_id == worktree_id {
1968                            selection.entry_id != entry.id
1969                        } else {
1970                            true
1971                        }
1972                    }))
1973                    && entry.is_dir()
1974            },
1975            cx,
1976        );
1977
1978        if let Some(selection) = selection {
1979            self.selection = Some(selection);
1980            self.autoscroll(cx);
1981            cx.notify();
1982        }
1983    }
1984
1985    fn select_next_directory(
1986        &mut self,
1987        _: &SelectNextDirectory,
1988        _: &mut Window,
1989        cx: &mut Context<Self>,
1990    ) {
1991        let selection = self.find_visible_entry(
1992            self.selection.as_ref(),
1993            false,
1994            |entry, worktree_id| {
1995                (self.selection.is_none()
1996                    || self.selection.is_some_and(|selection| {
1997                        if selection.worktree_id == worktree_id {
1998                            selection.entry_id != entry.id
1999                        } else {
2000                            true
2001                        }
2002                    }))
2003                    && entry.is_dir()
2004            },
2005            cx,
2006        );
2007
2008        if let Some(selection) = selection {
2009            self.selection = Some(selection);
2010            self.autoscroll(cx);
2011            cx.notify();
2012        }
2013    }
2014
2015    fn select_next_git_entry(
2016        &mut self,
2017        _: &SelectNextGitEntry,
2018        _: &mut Window,
2019        cx: &mut Context<Self>,
2020    ) {
2021        let selection = self.find_entry(
2022            self.selection.as_ref(),
2023            false,
2024            |entry, worktree_id| {
2025                (self.selection.is_none()
2026                    || self.selection.is_some_and(|selection| {
2027                        if selection.worktree_id == worktree_id {
2028                            selection.entry_id != entry.id
2029                        } else {
2030                            true
2031                        }
2032                    }))
2033                    && entry.is_file()
2034                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
2035            },
2036            cx,
2037        );
2038
2039        if let Some(selection) = selection {
2040            self.selection = Some(selection);
2041            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2042            self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
2043            self.autoscroll(cx);
2044            cx.notify();
2045        }
2046    }
2047
2048    fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
2049        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2050            if let Some(parent) = entry.path.parent() {
2051                let worktree = worktree.read(cx);
2052                if let Some(parent_entry) = worktree.entry_for_path(parent) {
2053                    self.selection = Some(SelectedEntry {
2054                        worktree_id: worktree.id(),
2055                        entry_id: parent_entry.id,
2056                    });
2057                    self.autoscroll(cx);
2058                    cx.notify();
2059                }
2060            }
2061        } else {
2062            self.select_first(&SelectFirst {}, window, cx);
2063        }
2064    }
2065
2066    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
2067        let worktree = self
2068            .visible_entries
2069            .first()
2070            .and_then(|(worktree_id, _, _)| {
2071                self.project.read(cx).worktree_for_id(*worktree_id, cx)
2072            });
2073        if let Some(worktree) = worktree {
2074            let worktree = worktree.read(cx);
2075            let worktree_id = worktree.id();
2076            if let Some(root_entry) = worktree.root_entry() {
2077                let selection = SelectedEntry {
2078                    worktree_id,
2079                    entry_id: root_entry.id,
2080                };
2081                self.selection = Some(selection);
2082                if window.modifiers().shift {
2083                    self.marked_entries.insert(selection);
2084                }
2085                self.autoscroll(cx);
2086                cx.notify();
2087            }
2088        }
2089    }
2090
2091    fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
2092        if let Some((worktree_id, visible_worktree_entries, _)) = self.visible_entries.last() {
2093            let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx);
2094            if let (Some(worktree), Some(entry)) = (worktree, visible_worktree_entries.last()) {
2095                let worktree = worktree.read(cx);
2096                if let Some(entry) = worktree.entry_for_id(entry.id) {
2097                    let selection = SelectedEntry {
2098                        worktree_id: *worktree_id,
2099                        entry_id: entry.id,
2100                    };
2101                    self.selection = Some(selection);
2102                    self.autoscroll(cx);
2103                    cx.notify();
2104                }
2105            }
2106        }
2107    }
2108
2109    fn autoscroll(&mut self, cx: &mut Context<Self>) {
2110        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2111            self.scroll_handle
2112                .scroll_to_item(index, ScrollStrategy::Center);
2113            cx.notify();
2114        }
2115    }
2116
2117    fn cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context<Self>) {
2118        let entries = self.disjoint_entries(cx);
2119        if !entries.is_empty() {
2120            self.clipboard = Some(ClipboardEntry::Cut(entries));
2121            cx.notify();
2122        }
2123    }
2124
2125    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2126        let entries = self.disjoint_entries(cx);
2127        if !entries.is_empty() {
2128            self.clipboard = Some(ClipboardEntry::Copied(entries));
2129            cx.notify();
2130        }
2131    }
2132
2133    fn create_paste_path(
2134        &self,
2135        source: &SelectedEntry,
2136        (worktree, target_entry): (Entity<Worktree>, &Entry),
2137        cx: &App,
2138    ) -> Option<(PathBuf, Option<Range<usize>>)> {
2139        let mut new_path = target_entry.path.to_path_buf();
2140        // If we're pasting into a file, or a directory into itself, go up one level.
2141        if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
2142            new_path.pop();
2143        }
2144        let clipboard_entry_file_name = self
2145            .project
2146            .read(cx)
2147            .path_for_entry(source.entry_id, cx)?
2148            .path
2149            .file_name()?
2150            .to_os_string();
2151        new_path.push(&clipboard_entry_file_name);
2152        let extension = new_path.extension().map(|e| e.to_os_string());
2153        let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
2154        let file_name_len = file_name_without_extension.to_string_lossy().len();
2155        let mut disambiguation_range = None;
2156        let mut ix = 0;
2157        {
2158            let worktree = worktree.read(cx);
2159            while worktree.entry_for_path(&new_path).is_some() {
2160                new_path.pop();
2161
2162                let mut new_file_name = file_name_without_extension.to_os_string();
2163
2164                let disambiguation = " copy";
2165                let mut disambiguation_len = disambiguation.len();
2166
2167                new_file_name.push(disambiguation);
2168
2169                if ix > 0 {
2170                    let extra_disambiguation = format!(" {}", ix);
2171                    disambiguation_len += extra_disambiguation.len();
2172
2173                    new_file_name.push(extra_disambiguation);
2174                }
2175                if let Some(extension) = extension.as_ref() {
2176                    new_file_name.push(".");
2177                    new_file_name.push(extension);
2178                }
2179
2180                new_path.push(new_file_name);
2181                disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
2182                ix += 1;
2183            }
2184        }
2185        Some((new_path, disambiguation_range))
2186    }
2187
2188    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
2189        maybe!({
2190            let (worktree, entry) = self.selected_entry_handle(cx)?;
2191            let entry = entry.clone();
2192            let worktree_id = worktree.read(cx).id();
2193            let clipboard_entries = self
2194                .clipboard
2195                .as_ref()
2196                .filter(|clipboard| !clipboard.items().is_empty())?;
2197            enum PasteTask {
2198                Rename(Task<Result<CreatedEntry>>),
2199                Copy(Task<Result<Option<Entry>>>),
2200            }
2201            let mut paste_entry_tasks: IndexMap<(ProjectEntryId, bool), PasteTask> =
2202                IndexMap::default();
2203            let mut disambiguation_range = None;
2204            let clip_is_cut = clipboard_entries.is_cut();
2205            for clipboard_entry in clipboard_entries.items() {
2206                let (new_path, new_disambiguation_range) =
2207                    self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
2208                let clip_entry_id = clipboard_entry.entry_id;
2209                let is_same_worktree = clipboard_entry.worktree_id == worktree_id;
2210                let relative_worktree_source_path = if !is_same_worktree {
2211                    let target_base_path = worktree.read(cx).abs_path();
2212                    let clipboard_project_path =
2213                        self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
2214                    let clipboard_abs_path = self
2215                        .project
2216                        .read(cx)
2217                        .absolute_path(&clipboard_project_path, cx)?;
2218                    Some(relativize_path(
2219                        &target_base_path,
2220                        clipboard_abs_path.as_path(),
2221                    ))
2222                } else {
2223                    None
2224                };
2225                let task = if clip_is_cut && is_same_worktree {
2226                    let task = self.project.update(cx, |project, cx| {
2227                        project.rename_entry(clip_entry_id, new_path, cx)
2228                    });
2229                    PasteTask::Rename(task)
2230                } else {
2231                    let entry_id = if is_same_worktree {
2232                        clip_entry_id
2233                    } else {
2234                        entry.id
2235                    };
2236                    let task = self.project.update(cx, |project, cx| {
2237                        project.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
2238                    });
2239                    PasteTask::Copy(task)
2240                };
2241                let needs_delete = !is_same_worktree && clip_is_cut;
2242                paste_entry_tasks.insert((clip_entry_id, needs_delete), task);
2243                disambiguation_range = new_disambiguation_range.or(disambiguation_range);
2244            }
2245
2246            let item_count = paste_entry_tasks.len();
2247
2248            cx.spawn_in(window, async move |project_panel, cx| {
2249                let mut last_succeed = None;
2250                let mut need_delete_ids = Vec::new();
2251                for ((entry_id, need_delete), task) in paste_entry_tasks.into_iter() {
2252                    match task {
2253                        PasteTask::Rename(task) => {
2254                            if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
2255                                last_succeed = Some(entry);
2256                            }
2257                        }
2258                        PasteTask::Copy(task) => {
2259                            if let Some(Some(entry)) = task.await.log_err() {
2260                                last_succeed = Some(entry);
2261                                if need_delete {
2262                                    need_delete_ids.push(entry_id);
2263                                }
2264                            }
2265                        }
2266                    }
2267                }
2268                // remove entry for cut in difference worktree
2269                for entry_id in need_delete_ids {
2270                    project_panel
2271                        .update(cx, |project_panel, cx| {
2272                            project_panel
2273                                .project
2274                                .update(cx, |project, cx| project.delete_entry(entry_id, true, cx))
2275                                .ok_or_else(|| anyhow!("no such entry"))
2276                        })??
2277                        .await?;
2278                }
2279                // update selection
2280                if let Some(entry) = last_succeed {
2281                    project_panel
2282                        .update_in(cx, |project_panel, window, cx| {
2283                            project_panel.selection = Some(SelectedEntry {
2284                                worktree_id,
2285                                entry_id: entry.id,
2286                            });
2287
2288                            if item_count == 1 {
2289                                // open entry if not dir, and only focus if rename is not pending
2290                                if !entry.is_dir() {
2291                                    project_panel.open_entry(
2292                                        entry.id,
2293                                        disambiguation_range.is_none(),
2294                                        false,
2295                                        cx,
2296                                    );
2297                                }
2298
2299                                // if only one entry was pasted and it was disambiguated, open the rename editor
2300                                if disambiguation_range.is_some() {
2301                                    cx.defer_in(window, |this, window, cx| {
2302                                        this.rename_impl(disambiguation_range, window, cx);
2303                                    });
2304                                }
2305                            }
2306                        })
2307                        .ok();
2308                }
2309
2310                anyhow::Ok(())
2311            })
2312            .detach_and_log_err(cx);
2313
2314            self.expand_entry(worktree_id, entry.id, cx);
2315            Some(())
2316        });
2317    }
2318
2319    fn duplicate(&mut self, _: &Duplicate, window: &mut Window, cx: &mut Context<Self>) {
2320        self.copy(&Copy {}, window, cx);
2321        self.paste(&Paste {}, window, cx);
2322    }
2323
2324    fn copy_path(
2325        &mut self,
2326        _: &zed_actions::workspace::CopyPath,
2327        _: &mut Window,
2328        cx: &mut Context<Self>,
2329    ) {
2330        let abs_file_paths = {
2331            let project = self.project.read(cx);
2332            self.effective_entries()
2333                .into_iter()
2334                .filter_map(|entry| {
2335                    let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
2336                    Some(
2337                        project
2338                            .worktree_for_id(entry.worktree_id, cx)?
2339                            .read(cx)
2340                            .abs_path()
2341                            .join(entry_path)
2342                            .to_string_lossy()
2343                            .to_string(),
2344                    )
2345                })
2346                .collect::<Vec<_>>()
2347        };
2348        if !abs_file_paths.is_empty() {
2349            cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
2350        }
2351    }
2352
2353    fn copy_relative_path(
2354        &mut self,
2355        _: &zed_actions::workspace::CopyRelativePath,
2356        _: &mut Window,
2357        cx: &mut Context<Self>,
2358    ) {
2359        let file_paths = {
2360            let project = self.project.read(cx);
2361            self.effective_entries()
2362                .into_iter()
2363                .filter_map(|entry| {
2364                    Some(
2365                        project
2366                            .path_for_entry(entry.entry_id, cx)?
2367                            .path
2368                            .to_string_lossy()
2369                            .to_string(),
2370                    )
2371                })
2372                .collect::<Vec<_>>()
2373        };
2374        if !file_paths.is_empty() {
2375            cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
2376        }
2377    }
2378
2379    fn reveal_in_finder(
2380        &mut self,
2381        _: &RevealInFileManager,
2382        _: &mut Window,
2383        cx: &mut Context<Self>,
2384    ) {
2385        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2386            cx.reveal_path(&worktree.read(cx).abs_path().join(&entry.path));
2387        }
2388    }
2389
2390    fn remove_from_project(
2391        &mut self,
2392        _: &RemoveFromProject,
2393        _window: &mut Window,
2394        cx: &mut Context<Self>,
2395    ) {
2396        for entry in self.effective_entries().iter() {
2397            let worktree_id = entry.worktree_id;
2398            self.project
2399                .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
2400        }
2401    }
2402
2403    fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
2404        if let Some((worktree, entry)) = self.selected_entry(cx) {
2405            let abs_path = worktree.abs_path().join(&entry.path);
2406            cx.open_with_system(&abs_path);
2407        }
2408    }
2409
2410    fn open_in_terminal(
2411        &mut self,
2412        _: &OpenInTerminal,
2413        window: &mut Window,
2414        cx: &mut Context<Self>,
2415    ) {
2416        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2417            let abs_path = match &entry.canonical_path {
2418                Some(canonical_path) => Some(canonical_path.to_path_buf()),
2419                None => worktree.read(cx).absolutize(&entry.path).ok(),
2420            };
2421
2422            let working_directory = if entry.is_dir() {
2423                abs_path
2424            } else {
2425                abs_path.and_then(|path| Some(path.parent()?.to_path_buf()))
2426            };
2427            if let Some(working_directory) = working_directory {
2428                window.dispatch_action(
2429                    workspace::OpenTerminal { working_directory }.boxed_clone(),
2430                    cx,
2431                )
2432            }
2433        }
2434    }
2435
2436    pub fn new_search_in_directory(
2437        &mut self,
2438        _: &NewSearchInDirectory,
2439        window: &mut Window,
2440        cx: &mut Context<Self>,
2441    ) {
2442        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2443            let dir_path = if entry.is_dir() {
2444                entry.path.clone()
2445            } else {
2446                // entry is a file, use its parent directory
2447                match entry.path.parent() {
2448                    Some(parent) => Arc::from(parent),
2449                    None => {
2450                        // File at root, open search with empty filter
2451                        self.workspace
2452                            .update(cx, |workspace, cx| {
2453                                search::ProjectSearchView::new_search_in_directory(
2454                                    workspace,
2455                                    Path::new(""),
2456                                    window,
2457                                    cx,
2458                                );
2459                            })
2460                            .ok();
2461                        return;
2462                    }
2463                }
2464            };
2465
2466            let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
2467            let dir_path = if include_root {
2468                let mut full_path = PathBuf::from(worktree.read(cx).root_name());
2469                full_path.push(&dir_path);
2470                Arc::from(full_path)
2471            } else {
2472                dir_path
2473            };
2474
2475            self.workspace
2476                .update(cx, |workspace, cx| {
2477                    search::ProjectSearchView::new_search_in_directory(
2478                        workspace, &dir_path, window, cx,
2479                    );
2480                })
2481                .ok();
2482        }
2483    }
2484
2485    fn move_entry(
2486        &mut self,
2487        entry_to_move: ProjectEntryId,
2488        destination: ProjectEntryId,
2489        destination_is_file: bool,
2490        cx: &mut Context<Self>,
2491    ) {
2492        if self
2493            .project
2494            .read(cx)
2495            .entry_is_worktree_root(entry_to_move, cx)
2496        {
2497            self.move_worktree_root(entry_to_move, destination, cx)
2498        } else {
2499            self.move_worktree_entry(entry_to_move, destination, destination_is_file, cx)
2500        }
2501    }
2502
2503    fn move_worktree_root(
2504        &mut self,
2505        entry_to_move: ProjectEntryId,
2506        destination: ProjectEntryId,
2507        cx: &mut Context<Self>,
2508    ) {
2509        self.project.update(cx, |project, cx| {
2510            let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
2511                return;
2512            };
2513            let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
2514                return;
2515            };
2516
2517            let worktree_id = worktree_to_move.read(cx).id();
2518            let destination_id = destination_worktree.read(cx).id();
2519
2520            project
2521                .move_worktree(worktree_id, destination_id, cx)
2522                .log_err();
2523        });
2524    }
2525
2526    fn move_worktree_entry(
2527        &mut self,
2528        entry_to_move: ProjectEntryId,
2529        destination: ProjectEntryId,
2530        destination_is_file: bool,
2531        cx: &mut Context<Self>,
2532    ) {
2533        if entry_to_move == destination {
2534            return;
2535        }
2536
2537        let destination_worktree = self.project.update(cx, |project, cx| {
2538            let entry_path = project.path_for_entry(entry_to_move, cx)?;
2539            let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
2540
2541            let mut destination_path = destination_entry_path.as_ref();
2542            if destination_is_file {
2543                destination_path = destination_path.parent()?;
2544            }
2545
2546            let mut new_path = destination_path.to_path_buf();
2547            new_path.push(entry_path.path.file_name()?);
2548            if new_path != entry_path.path.as_ref() {
2549                let task = project.rename_entry(entry_to_move, new_path, cx);
2550                cx.foreground_executor().spawn(task).detach_and_log_err(cx);
2551            }
2552
2553            project.worktree_id_for_entry(destination, cx)
2554        });
2555
2556        if let Some(destination_worktree) = destination_worktree {
2557            self.expand_entry(destination_worktree, destination, cx);
2558        }
2559    }
2560
2561    fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
2562        let mut entry_index = 0;
2563        let mut visible_entries_index = 0;
2564        for (worktree_index, (worktree_id, worktree_entries, _)) in
2565            self.visible_entries.iter().enumerate()
2566        {
2567            if *worktree_id == selection.worktree_id {
2568                for entry in worktree_entries {
2569                    if entry.id == selection.entry_id {
2570                        return Some((worktree_index, entry_index, visible_entries_index));
2571                    } else {
2572                        visible_entries_index += 1;
2573                        entry_index += 1;
2574                    }
2575                }
2576                break;
2577            } else {
2578                visible_entries_index += worktree_entries.len();
2579            }
2580        }
2581        None
2582    }
2583
2584    fn disjoint_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
2585        let marked_entries = self.effective_entries();
2586        let mut sanitized_entries = BTreeSet::new();
2587        if marked_entries.is_empty() {
2588            return sanitized_entries;
2589        }
2590
2591        let project = self.project.read(cx);
2592        let marked_entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = marked_entries
2593            .into_iter()
2594            .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
2595            .fold(HashMap::default(), |mut map, entry| {
2596                map.entry(entry.worktree_id).or_default().push(entry);
2597                map
2598            });
2599
2600        for (worktree_id, marked_entries) in marked_entries_by_worktree {
2601            if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
2602                let worktree = worktree.read(cx);
2603                let marked_dir_paths = marked_entries
2604                    .iter()
2605                    .filter_map(|entry| {
2606                        worktree.entry_for_id(entry.entry_id).and_then(|entry| {
2607                            if entry.is_dir() {
2608                                Some(entry.path.as_ref())
2609                            } else {
2610                                None
2611                            }
2612                        })
2613                    })
2614                    .collect::<BTreeSet<_>>();
2615
2616                sanitized_entries.extend(marked_entries.into_iter().filter(|entry| {
2617                    let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
2618                        return false;
2619                    };
2620                    let entry_path = entry_info.path.as_ref();
2621                    let inside_marked_dir = marked_dir_paths.iter().any(|&marked_dir_path| {
2622                        entry_path != marked_dir_path && entry_path.starts_with(marked_dir_path)
2623                    });
2624                    !inside_marked_dir
2625                }));
2626            }
2627        }
2628
2629        sanitized_entries
2630    }
2631
2632    fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
2633        if let Some(selection) = self.selection {
2634            let selection = SelectedEntry {
2635                entry_id: self.resolve_entry(selection.entry_id),
2636                worktree_id: selection.worktree_id,
2637            };
2638
2639            // Default to using just the selected item when nothing is marked.
2640            if self.marked_entries.is_empty() {
2641                return BTreeSet::from([selection]);
2642            }
2643
2644            // Allow operating on the selected item even when something else is marked,
2645            // making it easier to perform one-off actions without clearing a mark.
2646            if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
2647                return BTreeSet::from([selection]);
2648            }
2649        }
2650
2651        // Return only marked entries since we've already handled special cases where
2652        // only selection should take precedence. At this point, marked entries may or
2653        // may not include the current selection, which is intentional.
2654        self.marked_entries
2655            .iter()
2656            .map(|entry| SelectedEntry {
2657                entry_id: self.resolve_entry(entry.entry_id),
2658                worktree_id: entry.worktree_id,
2659            })
2660            .collect::<BTreeSet<_>>()
2661    }
2662
2663    /// Finds the currently selected subentry for a given leaf entry id. If a given entry
2664    /// has no ancestors, the project entry ID that's passed in is returned as-is.
2665    fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
2666        self.ancestors
2667            .get(&id)
2668            .and_then(|ancestors| {
2669                if ancestors.current_ancestor_depth == 0 {
2670                    return None;
2671                }
2672                ancestors.ancestors.get(ancestors.current_ancestor_depth)
2673            })
2674            .copied()
2675            .unwrap_or(id)
2676    }
2677
2678    pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
2679        let (worktree, entry) = self.selected_entry_handle(cx)?;
2680        Some((worktree.read(cx), entry))
2681    }
2682
2683    /// Compared to selected_entry, this function resolves to the currently
2684    /// selected subentry if dir auto-folding is enabled.
2685    fn selected_sub_entry<'a>(
2686        &self,
2687        cx: &'a App,
2688    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2689        let (worktree, mut entry) = self.selected_entry_handle(cx)?;
2690
2691        let resolved_id = self.resolve_entry(entry.id);
2692        if resolved_id != entry.id {
2693            let worktree = worktree.read(cx);
2694            entry = worktree.entry_for_id(resolved_id)?;
2695        }
2696        Some((worktree, entry))
2697    }
2698    fn selected_entry_handle<'a>(
2699        &self,
2700        cx: &'a App,
2701    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2702        let selection = self.selection?;
2703        let project = self.project.read(cx);
2704        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
2705        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
2706        Some((worktree, entry))
2707    }
2708
2709    fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
2710        let (worktree, entry) = self.selected_entry(cx)?;
2711        let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
2712
2713        for path in entry.path.ancestors() {
2714            let Some(entry) = worktree.entry_for_path(path) else {
2715                continue;
2716            };
2717            if entry.is_dir() {
2718                if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
2719                    expanded_dir_ids.insert(idx, entry.id);
2720                }
2721            }
2722        }
2723
2724        Some(())
2725    }
2726
2727    fn update_visible_entries(
2728        &mut self,
2729        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
2730        cx: &mut Context<Self>,
2731    ) {
2732        let settings = ProjectPanelSettings::get_global(cx);
2733        let auto_collapse_dirs = settings.auto_fold_dirs;
2734        let hide_gitignore = settings.hide_gitignore;
2735        let project = self.project.read(cx);
2736        let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
2737        self.last_worktree_root_id = project
2738            .visible_worktrees(cx)
2739            .next_back()
2740            .and_then(|worktree| worktree.read(cx).root_entry())
2741            .map(|entry| entry.id);
2742
2743        let old_ancestors = std::mem::take(&mut self.ancestors);
2744        self.visible_entries.clear();
2745        let mut max_width_item = None;
2746        for worktree in project.visible_worktrees(cx) {
2747            let worktree_snapshot = worktree.read(cx).snapshot();
2748            let worktree_id = worktree_snapshot.id();
2749
2750            let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
2751                hash_map::Entry::Occupied(e) => e.into_mut(),
2752                hash_map::Entry::Vacant(e) => {
2753                    // The first time a worktree's root entry becomes available,
2754                    // mark that root entry as expanded.
2755                    if let Some(entry) = worktree_snapshot.root_entry() {
2756                        e.insert(vec![entry.id]).as_slice()
2757                    } else {
2758                        &[]
2759                    }
2760                }
2761            };
2762
2763            let mut new_entry_parent_id = None;
2764            let mut new_entry_kind = EntryKind::Dir;
2765            if let Some(edit_state) = &self.edit_state {
2766                if edit_state.worktree_id == worktree_id && edit_state.is_new_entry() {
2767                    new_entry_parent_id = Some(edit_state.entry_id);
2768                    new_entry_kind = if edit_state.is_dir {
2769                        EntryKind::Dir
2770                    } else {
2771                        EntryKind::File
2772                    };
2773                }
2774            }
2775
2776            let mut visible_worktree_entries = Vec::new();
2777            let mut entry_iter =
2778                GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
2779            let mut auto_folded_ancestors = vec![];
2780            while let Some(entry) = entry_iter.entry() {
2781                if auto_collapse_dirs && entry.kind.is_dir() {
2782                    auto_folded_ancestors.push(entry.id);
2783                    if !self.unfolded_dir_ids.contains(&entry.id) {
2784                        if let Some(root_path) = worktree_snapshot.root_entry() {
2785                            let mut child_entries = worktree_snapshot.child_entries(&entry.path);
2786                            if let Some(child) = child_entries.next() {
2787                                if entry.path != root_path.path
2788                                    && child_entries.next().is_none()
2789                                    && child.kind.is_dir()
2790                                {
2791                                    entry_iter.advance();
2792
2793                                    continue;
2794                                }
2795                            }
2796                        }
2797                    }
2798                    let depth = old_ancestors
2799                        .get(&entry.id)
2800                        .map(|ancestor| ancestor.current_ancestor_depth)
2801                        .unwrap_or_default()
2802                        .min(auto_folded_ancestors.len());
2803                    if let Some(edit_state) = &mut self.edit_state {
2804                        if edit_state.entry_id == entry.id {
2805                            edit_state.depth = depth;
2806                        }
2807                    }
2808                    let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
2809                    if ancestors.len() > 1 {
2810                        ancestors.reverse();
2811                        self.ancestors.insert(
2812                            entry.id,
2813                            FoldedAncestors {
2814                                current_ancestor_depth: depth,
2815                                ancestors,
2816                            },
2817                        );
2818                    }
2819                }
2820                auto_folded_ancestors.clear();
2821                if !hide_gitignore || !entry.is_ignored {
2822                    visible_worktree_entries.push(entry.to_owned());
2823                }
2824                let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
2825                    entry.id == new_entry_id || {
2826                        self.ancestors.get(&entry.id).map_or(false, |entries| {
2827                            entries
2828                                .ancestors
2829                                .iter()
2830                                .any(|entry_id| *entry_id == new_entry_id)
2831                        })
2832                    }
2833                } else {
2834                    false
2835                };
2836                if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
2837                    visible_worktree_entries.push(GitEntry {
2838                        entry: Entry {
2839                            id: NEW_ENTRY_ID,
2840                            kind: new_entry_kind,
2841                            path: entry.path.join("\0").into(),
2842                            inode: 0,
2843                            mtime: entry.mtime,
2844                            size: entry.size,
2845                            is_ignored: entry.is_ignored,
2846                            is_external: false,
2847                            is_private: false,
2848                            is_always_included: entry.is_always_included,
2849                            canonical_path: entry.canonical_path.clone(),
2850                            char_bag: entry.char_bag,
2851                            is_fifo: entry.is_fifo,
2852                        },
2853                        git_summary: entry.git_summary,
2854                    });
2855                }
2856                let worktree_abs_path = worktree.read(cx).abs_path();
2857                let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
2858                    let Some(path_name) = worktree_abs_path
2859                        .file_name()
2860                        .with_context(|| {
2861                            format!("Worktree abs path has no file name, root entry: {entry:?}")
2862                        })
2863                        .log_err()
2864                    else {
2865                        continue;
2866                    };
2867                    let path = ArcCow::Borrowed(Path::new(path_name));
2868                    let depth = 0;
2869                    (depth, path)
2870                } else if entry.is_file() {
2871                    let Some(path_name) = entry
2872                        .path
2873                        .file_name()
2874                        .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
2875                        .log_err()
2876                    else {
2877                        continue;
2878                    };
2879                    let path = ArcCow::Borrowed(Path::new(path_name));
2880                    let depth = entry.path.ancestors().count() - 1;
2881                    (depth, path)
2882                } else {
2883                    let path = self
2884                        .ancestors
2885                        .get(&entry.id)
2886                        .and_then(|ancestors| {
2887                            let outermost_ancestor = ancestors.ancestors.last()?;
2888                            let root_folded_entry = worktree
2889                                .read(cx)
2890                                .entry_for_id(*outermost_ancestor)?
2891                                .path
2892                                .as_ref();
2893                            entry
2894                                .path
2895                                .strip_prefix(root_folded_entry)
2896                                .ok()
2897                                .and_then(|suffix| {
2898                                    let full_path = Path::new(root_folded_entry.file_name()?);
2899                                    Some(ArcCow::Owned(Arc::<Path>::from(full_path.join(suffix))))
2900                                })
2901                        })
2902                        .or_else(|| entry.path.file_name().map(Path::new).map(ArcCow::Borrowed))
2903                        .unwrap_or_else(|| ArcCow::Owned(entry.path.clone()));
2904                    let depth = path.components().count();
2905                    (depth, path)
2906                };
2907                let width_estimate = item_width_estimate(
2908                    depth,
2909                    path.to_string_lossy().chars().count(),
2910                    entry.canonical_path.is_some(),
2911                );
2912
2913                match max_width_item.as_mut() {
2914                    Some((id, worktree_id, width)) => {
2915                        if *width < width_estimate {
2916                            *id = entry.id;
2917                            *worktree_id = worktree.read(cx).id();
2918                            *width = width_estimate;
2919                        }
2920                    }
2921                    None => {
2922                        max_width_item = Some((entry.id, worktree.read(cx).id(), width_estimate))
2923                    }
2924                }
2925
2926                if expanded_dir_ids.binary_search(&entry.id).is_err()
2927                    && entry_iter.advance_to_sibling()
2928                {
2929                    continue;
2930                }
2931                entry_iter.advance();
2932            }
2933
2934            project::sort_worktree_entries(&mut visible_worktree_entries);
2935
2936            self.visible_entries
2937                .push((worktree_id, visible_worktree_entries, OnceCell::new()));
2938        }
2939
2940        if let Some((project_entry_id, worktree_id, _)) = max_width_item {
2941            let mut visited_worktrees_length = 0;
2942            let index = self.visible_entries.iter().find_map(|(id, entries, _)| {
2943                if worktree_id == *id {
2944                    entries
2945                        .iter()
2946                        .position(|entry| entry.id == project_entry_id)
2947                } else {
2948                    visited_worktrees_length += entries.len();
2949                    None
2950                }
2951            });
2952            if let Some(index) = index {
2953                self.max_width_item_index = Some(visited_worktrees_length + index);
2954            }
2955        }
2956        if let Some((worktree_id, entry_id)) = new_selected_entry {
2957            self.selection = Some(SelectedEntry {
2958                worktree_id,
2959                entry_id,
2960            });
2961        }
2962    }
2963
2964    fn expand_entry(
2965        &mut self,
2966        worktree_id: WorktreeId,
2967        entry_id: ProjectEntryId,
2968        cx: &mut Context<Self>,
2969    ) {
2970        self.project.update(cx, |project, cx| {
2971            if let Some((worktree, expanded_dir_ids)) = project
2972                .worktree_for_id(worktree_id, cx)
2973                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
2974            {
2975                project.expand_entry(worktree_id, entry_id, cx);
2976                let worktree = worktree.read(cx);
2977
2978                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
2979                    loop {
2980                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
2981                            expanded_dir_ids.insert(ix, entry.id);
2982                        }
2983
2984                        if let Some(parent_entry) =
2985                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
2986                        {
2987                            entry = parent_entry;
2988                        } else {
2989                            break;
2990                        }
2991                    }
2992                }
2993            }
2994        });
2995    }
2996
2997    fn drop_external_files(
2998        &mut self,
2999        paths: &[PathBuf],
3000        entry_id: ProjectEntryId,
3001        window: &mut Window,
3002        cx: &mut Context<Self>,
3003    ) {
3004        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
3005
3006        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
3007
3008        let Some((target_directory, worktree, fs)) = maybe!({
3009            let project = self.project.read(cx);
3010            let fs = project.fs().clone();
3011            let worktree = project.worktree_for_entry(entry_id, cx)?;
3012            let entry = worktree.read(cx).entry_for_id(entry_id)?;
3013            let path = entry.path.clone();
3014            let target_directory = if entry.is_dir() {
3015                path.to_path_buf()
3016            } else {
3017                path.parent()?.to_path_buf()
3018            };
3019            Some((target_directory, worktree, fs))
3020        }) else {
3021            return;
3022        };
3023
3024        let mut paths_to_replace = Vec::new();
3025        for path in &paths {
3026            if let Some(name) = path.file_name() {
3027                let mut target_path = target_directory.clone();
3028                target_path.push(name);
3029                if target_path.exists() {
3030                    paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
3031                }
3032            }
3033        }
3034
3035        cx.spawn_in(window, async move |this, cx| {
3036            async move {
3037                for (filename, original_path) in &paths_to_replace {
3038                    let answer = cx.update(|window, cx| {
3039                        window
3040                            .prompt(
3041                                PromptLevel::Info,
3042                                format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(),
3043                                None,
3044                                &["Replace", "Cancel"],
3045                                cx,
3046                            )
3047                    })?.await?;
3048
3049                    if answer == 1 {
3050                        if let Some(item_idx) = paths.iter().position(|p| p == original_path) {
3051                            paths.remove(item_idx);
3052                        }
3053                    }
3054                }
3055
3056                if paths.is_empty() {
3057                    return Ok(());
3058                }
3059
3060                let task = worktree.update( cx, |worktree, cx| {
3061                    worktree.copy_external_entries(target_directory.into(), paths, fs, cx)
3062                })?;
3063
3064                let opened_entries = task.await.with_context(|| "failed to copy external paths")?;
3065                this.update(cx, |this, cx| {
3066                    if open_file_after_drop && !opened_entries.is_empty() {
3067                        this.open_entry(opened_entries[0], true, false, cx);
3068                    }
3069                })
3070            }
3071            .log_err().await
3072        })
3073        .detach();
3074    }
3075
3076    fn drag_onto(
3077        &mut self,
3078        selections: &DraggedSelection,
3079        target_entry_id: ProjectEntryId,
3080        is_file: bool,
3081        window: &mut Window,
3082        cx: &mut Context<Self>,
3083    ) {
3084        let should_copy = window.modifiers().alt;
3085        if should_copy {
3086            let _ = maybe!({
3087                let project = self.project.read(cx);
3088                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
3089                let worktree_id = target_worktree.read(cx).id();
3090                let target_entry = target_worktree
3091                    .read(cx)
3092                    .entry_for_id(target_entry_id)?
3093                    .clone();
3094
3095                let mut copy_tasks = Vec::new();
3096                let mut disambiguation_range = None;
3097                for selection in selections.items() {
3098                    let (new_path, new_disambiguation_range) = self.create_paste_path(
3099                        selection,
3100                        (target_worktree.clone(), &target_entry),
3101                        cx,
3102                    )?;
3103
3104                    let task = self.project.update(cx, |project, cx| {
3105                        project.copy_entry(selection.entry_id, None, new_path, cx)
3106                    });
3107                    copy_tasks.push(task);
3108                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
3109                }
3110
3111                let item_count = copy_tasks.len();
3112
3113                cx.spawn_in(window, async move |project_panel, cx| {
3114                    let mut last_succeed = None;
3115                    for task in copy_tasks.into_iter() {
3116                        if let Some(Some(entry)) = task.await.log_err() {
3117                            last_succeed = Some(entry.id);
3118                        }
3119                    }
3120                    // update selection
3121                    if let Some(entry_id) = last_succeed {
3122                        project_panel
3123                            .update_in(cx, |project_panel, window, cx| {
3124                                project_panel.selection = Some(SelectedEntry {
3125                                    worktree_id,
3126                                    entry_id,
3127                                });
3128
3129                                // if only one entry was dragged and it was disambiguated, open the rename editor
3130                                if item_count == 1 && disambiguation_range.is_some() {
3131                                    project_panel.rename_impl(disambiguation_range, window, cx);
3132                                }
3133                            })
3134                            .ok();
3135                    }
3136                })
3137                .detach();
3138                Some(())
3139            });
3140        } else {
3141            for selection in selections.items() {
3142                self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
3143            }
3144        }
3145    }
3146
3147    fn index_for_entry(
3148        &self,
3149        entry_id: ProjectEntryId,
3150        worktree_id: WorktreeId,
3151    ) -> Option<(usize, usize, usize)> {
3152        let mut worktree_ix = 0;
3153        let mut total_ix = 0;
3154        for (current_worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3155            if worktree_id != *current_worktree_id {
3156                total_ix += visible_worktree_entries.len();
3157                worktree_ix += 1;
3158                continue;
3159            }
3160
3161            return visible_worktree_entries
3162                .iter()
3163                .enumerate()
3164                .find(|(_, entry)| entry.id == entry_id)
3165                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
3166        }
3167        None
3168    }
3169
3170    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
3171        let mut offset = 0;
3172        for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3173            if visible_worktree_entries.len() > offset + index {
3174                return visible_worktree_entries
3175                    .get(index)
3176                    .map(|entry| (*worktree_id, entry.to_ref()));
3177            }
3178            offset += visible_worktree_entries.len();
3179        }
3180        None
3181    }
3182
3183    fn iter_visible_entries(
3184        &self,
3185        range: Range<usize>,
3186        window: &mut Window,
3187        cx: &mut Context<ProjectPanel>,
3188        mut callback: impl FnMut(&Entry, &HashSet<Arc<Path>>, &mut Window, &mut Context<ProjectPanel>),
3189    ) {
3190        let mut ix = 0;
3191        for (_, visible_worktree_entries, entries_paths) in &self.visible_entries {
3192            if ix >= range.end {
3193                return;
3194            }
3195
3196            if ix + visible_worktree_entries.len() <= range.start {
3197                ix += visible_worktree_entries.len();
3198                continue;
3199            }
3200
3201            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3202            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3203            let entries = entries_paths.get_or_init(|| {
3204                visible_worktree_entries
3205                    .iter()
3206                    .map(|e| (e.path.clone()))
3207                    .collect()
3208            });
3209            for entry in visible_worktree_entries[entry_range].iter() {
3210                callback(&entry, entries, window, cx);
3211            }
3212            ix = end_ix;
3213        }
3214    }
3215
3216    fn for_each_visible_entry(
3217        &self,
3218        range: Range<usize>,
3219        window: &mut Window,
3220        cx: &mut Context<ProjectPanel>,
3221        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>),
3222    ) {
3223        let mut ix = 0;
3224        for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
3225            if ix >= range.end {
3226                return;
3227            }
3228
3229            if ix + visible_worktree_entries.len() <= range.start {
3230                ix += visible_worktree_entries.len();
3231                continue;
3232            }
3233
3234            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3235            let (git_status_setting, show_file_icons, show_folder_icons) = {
3236                let settings = ProjectPanelSettings::get_global(cx);
3237                (
3238                    settings.git_status,
3239                    settings.file_icons,
3240                    settings.folder_icons,
3241                )
3242            };
3243            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
3244                let snapshot = worktree.read(cx).snapshot();
3245                let root_name = OsStr::new(snapshot.root_name());
3246                let expanded_entry_ids = self
3247                    .expanded_dir_ids
3248                    .get(&snapshot.id())
3249                    .map(Vec::as_slice)
3250                    .unwrap_or(&[]);
3251
3252                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3253                let entries = entries_paths.get_or_init(|| {
3254                    visible_worktree_entries
3255                        .iter()
3256                        .map(|e| (e.path.clone()))
3257                        .collect()
3258                });
3259                for entry in visible_worktree_entries[entry_range].iter() {
3260                    let status = git_status_setting
3261                        .then_some(entry.git_summary)
3262                        .unwrap_or_default();
3263                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
3264                    let icon = match entry.kind {
3265                        EntryKind::File => {
3266                            if show_file_icons {
3267                                FileIcons::get_icon(&entry.path, cx)
3268                            } else {
3269                                None
3270                            }
3271                        }
3272                        _ => {
3273                            if show_folder_icons {
3274                                FileIcons::get_folder_icon(is_expanded, cx)
3275                            } else {
3276                                FileIcons::get_chevron_icon(is_expanded, cx)
3277                            }
3278                        }
3279                    };
3280
3281                    let (depth, difference) =
3282                        ProjectPanel::calculate_depth_and_difference(&entry, entries);
3283
3284                    let filename = match difference {
3285                        diff if diff > 1 => entry
3286                            .path
3287                            .iter()
3288                            .skip(entry.path.components().count() - diff)
3289                            .collect::<PathBuf>()
3290                            .to_str()
3291                            .unwrap_or_default()
3292                            .to_string(),
3293                        _ => entry
3294                            .path
3295                            .file_name()
3296                            .map(|name| name.to_string_lossy().into_owned())
3297                            .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
3298                    };
3299                    let selection = SelectedEntry {
3300                        worktree_id: snapshot.id(),
3301                        entry_id: entry.id,
3302                    };
3303
3304                    let is_marked = self.marked_entries.contains(&selection);
3305
3306                    let diagnostic_severity = self
3307                        .diagnostics
3308                        .get(&(*worktree_id, entry.path.to_path_buf()))
3309                        .cloned();
3310
3311                    let filename_text_color =
3312                        entry_git_aware_label_color(status, entry.is_ignored, is_marked);
3313
3314                    let mut details = EntryDetails {
3315                        filename,
3316                        icon,
3317                        path: entry.path.clone(),
3318                        depth,
3319                        kind: entry.kind,
3320                        is_ignored: entry.is_ignored,
3321                        is_expanded,
3322                        is_selected: self.selection == Some(selection),
3323                        is_marked,
3324                        is_editing: false,
3325                        is_processing: false,
3326                        is_cut: self
3327                            .clipboard
3328                            .as_ref()
3329                            .map_or(false, |e| e.is_cut() && e.items().contains(&selection)),
3330                        filename_text_color,
3331                        diagnostic_severity,
3332                        git_status: status,
3333                        is_private: entry.is_private,
3334                        worktree_id: *worktree_id,
3335                        canonical_path: entry.canonical_path.clone(),
3336                    };
3337
3338                    if let Some(edit_state) = &self.edit_state {
3339                        let is_edited_entry = if edit_state.is_new_entry() {
3340                            entry.id == NEW_ENTRY_ID
3341                        } else {
3342                            entry.id == edit_state.entry_id
3343                                || self
3344                                    .ancestors
3345                                    .get(&entry.id)
3346                                    .is_some_and(|auto_folded_dirs| {
3347                                        auto_folded_dirs
3348                                            .ancestors
3349                                            .iter()
3350                                            .any(|entry_id| *entry_id == edit_state.entry_id)
3351                                    })
3352                        };
3353
3354                        if is_edited_entry {
3355                            if let Some(processing_filename) = &edit_state.processing_filename {
3356                                details.is_processing = true;
3357                                if let Some(ancestors) = edit_state
3358                                    .leaf_entry_id
3359                                    .and_then(|entry| self.ancestors.get(&entry))
3360                                {
3361                                    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;
3362                                    let all_components = ancestors.ancestors.len();
3363
3364                                    let prefix_components = all_components - position;
3365                                    let suffix_components = position.checked_sub(1);
3366                                    let mut previous_components =
3367                                        Path::new(&details.filename).components();
3368                                    let mut new_path = previous_components
3369                                        .by_ref()
3370                                        .take(prefix_components)
3371                                        .collect::<PathBuf>();
3372                                    if let Some(last_component) =
3373                                        Path::new(processing_filename).components().next_back()
3374                                    {
3375                                        new_path.push(last_component);
3376                                        previous_components.next();
3377                                    }
3378
3379                                    if let Some(_) = suffix_components {
3380                                        new_path.push(previous_components);
3381                                    }
3382                                    if let Some(str) = new_path.to_str() {
3383                                        details.filename.clear();
3384                                        details.filename.push_str(str);
3385                                    }
3386                                } else {
3387                                    details.filename.clear();
3388                                    details.filename.push_str(processing_filename);
3389                                }
3390                            } else {
3391                                if edit_state.is_new_entry() {
3392                                    details.filename.clear();
3393                                }
3394                                details.is_editing = true;
3395                            }
3396                        }
3397                    }
3398
3399                    callback(entry.id, details, window, cx);
3400                }
3401            }
3402            ix = end_ix;
3403        }
3404    }
3405
3406    fn find_entry_in_worktree(
3407        &self,
3408        worktree_id: WorktreeId,
3409        reverse_search: bool,
3410        only_visible_entries: bool,
3411        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3412        cx: &mut Context<Self>,
3413    ) -> Option<GitEntry> {
3414        if only_visible_entries {
3415            let entries = self
3416                .visible_entries
3417                .iter()
3418                .find_map(|(tree_id, entries, _)| {
3419                    if worktree_id == *tree_id {
3420                        Some(entries)
3421                    } else {
3422                        None
3423                    }
3424                })?
3425                .clone();
3426
3427            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
3428                .find(|ele| predicate(ele.to_ref(), worktree_id))
3429                .cloned();
3430        }
3431
3432        let repo_snapshots = self
3433            .project
3434            .read(cx)
3435            .git_store()
3436            .read(cx)
3437            .repo_snapshots(cx);
3438        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
3439        worktree.update(cx, |tree, _| {
3440            utils::ReversibleIterable::new(
3441                GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
3442                reverse_search,
3443            )
3444            .find_single_ended(|ele| predicate(*ele, worktree_id))
3445            .map(|ele| ele.to_owned())
3446        })
3447    }
3448
3449    fn find_entry(
3450        &self,
3451        start: Option<&SelectedEntry>,
3452        reverse_search: bool,
3453        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3454        cx: &mut Context<Self>,
3455    ) -> Option<SelectedEntry> {
3456        let mut worktree_ids: Vec<_> = self
3457            .visible_entries
3458            .iter()
3459            .map(|(worktree_id, _, _)| *worktree_id)
3460            .collect();
3461        let repo_snapshots = self
3462            .project
3463            .read(cx)
3464            .git_store()
3465            .read(cx)
3466            .repo_snapshots(cx);
3467
3468        let mut last_found: Option<SelectedEntry> = None;
3469
3470        if let Some(start) = start {
3471            let worktree = self
3472                .project
3473                .read(cx)
3474                .worktree_for_id(start.worktree_id, cx)?;
3475
3476            let search = worktree.update(cx, |tree, _| {
3477                let entry = tree.entry_for_id(start.entry_id)?;
3478                let root_entry = tree.root_entry()?;
3479                let tree_id = tree.id();
3480
3481                let mut first_iter = GitTraversal::new(
3482                    &repo_snapshots,
3483                    tree.traverse_from_path(true, true, true, entry.path.as_ref()),
3484                );
3485
3486                if reverse_search {
3487                    first_iter.next();
3488                }
3489
3490                let first = first_iter
3491                    .enumerate()
3492                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
3493                    .map(|(_, entry)| entry)
3494                    .find(|ele| predicate(*ele, tree_id))
3495                    .map(|ele| ele.to_owned());
3496
3497                let second_iter = GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize));
3498
3499                let second = if reverse_search {
3500                    second_iter
3501                        .take_until(|ele| ele.id == start.entry_id)
3502                        .filter(|ele| predicate(*ele, tree_id))
3503                        .last()
3504                        .map(|ele| ele.to_owned())
3505                } else {
3506                    second_iter
3507                        .take_while(|ele| ele.id != start.entry_id)
3508                        .filter(|ele| predicate(*ele, tree_id))
3509                        .last()
3510                        .map(|ele| ele.to_owned())
3511                };
3512
3513                if reverse_search {
3514                    Some((second, first))
3515                } else {
3516                    Some((first, second))
3517                }
3518            });
3519
3520            if let Some((first, second)) = search {
3521                let first = first.map(|entry| SelectedEntry {
3522                    worktree_id: start.worktree_id,
3523                    entry_id: entry.id,
3524                });
3525
3526                let second = second.map(|entry| SelectedEntry {
3527                    worktree_id: start.worktree_id,
3528                    entry_id: entry.id,
3529                });
3530
3531                if first.is_some() {
3532                    return first;
3533                }
3534                last_found = second;
3535
3536                let idx = worktree_ids
3537                    .iter()
3538                    .enumerate()
3539                    .find(|(_, ele)| **ele == start.worktree_id)
3540                    .map(|(idx, _)| idx);
3541
3542                if let Some(idx) = idx {
3543                    worktree_ids.rotate_left(idx + 1usize);
3544                    worktree_ids.pop();
3545                }
3546            }
3547        }
3548
3549        for tree_id in worktree_ids.into_iter() {
3550            if let Some(found) =
3551                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
3552            {
3553                return Some(SelectedEntry {
3554                    worktree_id: tree_id,
3555                    entry_id: found.id,
3556                });
3557            }
3558        }
3559
3560        last_found
3561    }
3562
3563    fn find_visible_entry(
3564        &self,
3565        start: Option<&SelectedEntry>,
3566        reverse_search: bool,
3567        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3568        cx: &mut Context<Self>,
3569    ) -> Option<SelectedEntry> {
3570        let mut worktree_ids: Vec<_> = self
3571            .visible_entries
3572            .iter()
3573            .map(|(worktree_id, _, _)| *worktree_id)
3574            .collect();
3575
3576        let mut last_found: Option<SelectedEntry> = None;
3577
3578        if let Some(start) = start {
3579            let entries = self
3580                .visible_entries
3581                .iter()
3582                .find(|(worktree_id, _, _)| *worktree_id == start.worktree_id)
3583                .map(|(_, entries, _)| entries)?;
3584
3585            let mut start_idx = entries
3586                .iter()
3587                .enumerate()
3588                .find(|(_, ele)| ele.id == start.entry_id)
3589                .map(|(idx, _)| idx)?;
3590
3591            if reverse_search {
3592                start_idx = start_idx.saturating_add(1usize);
3593            }
3594
3595            let (left, right) = entries.split_at_checked(start_idx)?;
3596
3597            let (first_iter, second_iter) = if reverse_search {
3598                (
3599                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3600                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3601                )
3602            } else {
3603                (
3604                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3605                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3606                )
3607            };
3608
3609            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3610            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3611
3612            if first_search.is_some() {
3613                return first_search.map(|entry| SelectedEntry {
3614                    worktree_id: start.worktree_id,
3615                    entry_id: entry.id,
3616                });
3617            }
3618
3619            last_found = second_search.map(|entry| SelectedEntry {
3620                worktree_id: start.worktree_id,
3621                entry_id: entry.id,
3622            });
3623
3624            let idx = worktree_ids
3625                .iter()
3626                .enumerate()
3627                .find(|(_, ele)| **ele == start.worktree_id)
3628                .map(|(idx, _)| idx);
3629
3630            if let Some(idx) = idx {
3631                worktree_ids.rotate_left(idx + 1usize);
3632                worktree_ids.pop();
3633            }
3634        }
3635
3636        for tree_id in worktree_ids.into_iter() {
3637            if let Some(found) =
3638                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
3639            {
3640                return Some(SelectedEntry {
3641                    worktree_id: tree_id,
3642                    entry_id: found.id,
3643                });
3644            }
3645        }
3646
3647        last_found
3648    }
3649
3650    fn calculate_depth_and_difference(
3651        entry: &Entry,
3652        visible_worktree_entries: &HashSet<Arc<Path>>,
3653    ) -> (usize, usize) {
3654        let (depth, difference) = entry
3655            .path
3656            .ancestors()
3657            .skip(1) // Skip the entry itself
3658            .find_map(|ancestor| {
3659                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
3660                    let entry_path_components_count = entry.path.components().count();
3661                    let parent_path_components_count = parent_entry.components().count();
3662                    let difference = entry_path_components_count - parent_path_components_count;
3663                    let depth = parent_entry
3664                        .ancestors()
3665                        .skip(1)
3666                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
3667                        .count();
3668                    Some((depth + 1, difference))
3669                } else {
3670                    None
3671                }
3672            })
3673            .unwrap_or((0, 0));
3674
3675        (depth, difference)
3676    }
3677
3678    fn render_entry(
3679        &self,
3680        entry_id: ProjectEntryId,
3681        details: EntryDetails,
3682        window: &mut Window,
3683        cx: &mut Context<Self>,
3684    ) -> Stateful<Div> {
3685        const GROUP_NAME: &str = "project_entry";
3686
3687        let kind = details.kind;
3688        let settings = ProjectPanelSettings::get_global(cx);
3689        let show_editor = details.is_editing && !details.is_processing;
3690
3691        let selection = SelectedEntry {
3692            worktree_id: details.worktree_id,
3693            entry_id,
3694        };
3695
3696        let is_marked = self.marked_entries.contains(&selection);
3697        let is_active = self
3698            .selection
3699            .map_or(false, |selection| selection.entry_id == entry_id);
3700
3701        let file_name = details.filename.clone();
3702
3703        let mut icon = details.icon.clone();
3704        if settings.file_icons && show_editor && details.kind.is_file() {
3705            let filename = self.filename_editor.read(cx).text(cx);
3706            if filename.len() > 2 {
3707                icon = FileIcons::get_icon(Path::new(&filename), cx);
3708            }
3709        }
3710
3711        let filename_text_color = details.filename_text_color;
3712        let diagnostic_severity = details.diagnostic_severity;
3713        let item_colors = get_item_color(cx);
3714
3715        let canonical_path = details
3716            .canonical_path
3717            .as_ref()
3718            .map(|f| f.to_string_lossy().to_string());
3719        let path = details.path.clone();
3720
3721        let depth = details.depth;
3722        let worktree_id = details.worktree_id;
3723        let selections = Arc::new(self.marked_entries.clone());
3724
3725        let dragged_selection = DraggedSelection {
3726            active_selection: selection,
3727            marked_selections: selections,
3728        };
3729
3730        let bg_color = if is_marked {
3731            item_colors.marked
3732        } else {
3733            item_colors.default
3734        };
3735
3736        let bg_hover_color = if is_marked {
3737            item_colors.marked
3738        } else {
3739            item_colors.hover
3740        };
3741
3742        let validation_color_and_message = if show_editor {
3743            match self
3744                .edit_state
3745                .as_ref()
3746                .map_or(ValidationState::None, |e| e.validation_state.clone())
3747            {
3748                ValidationState::Error(msg) => Some((Color::Error.color(cx), msg.clone())),
3749                ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg.clone())),
3750                ValidationState::None => None,
3751            }
3752        } else {
3753            None
3754        };
3755
3756        let border_color =
3757            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3758                match validation_color_and_message {
3759                    Some((color, _)) => color,
3760                    None => item_colors.focused,
3761                }
3762            } else {
3763                bg_color
3764            };
3765
3766        let border_hover_color =
3767            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3768                match validation_color_and_message {
3769                    Some((color, _)) => color,
3770                    None => item_colors.focused,
3771                }
3772            } else {
3773                bg_hover_color
3774            };
3775
3776        let folded_directory_drag_target = self.folded_directory_drag_target;
3777
3778        div()
3779            .id(entry_id.to_proto() as usize)
3780            .group(GROUP_NAME)
3781            .cursor_pointer()
3782            .rounded_none()
3783            .bg(bg_color)
3784            .border_1()
3785            .border_r_2()
3786            .border_color(border_color)
3787            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
3788            .on_drag_move::<ExternalPaths>(cx.listener(
3789                move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
3790                    if event.bounds.contains(&event.event.position) {
3791                        if this.last_external_paths_drag_over_entry == Some(entry_id) {
3792                            return;
3793                        }
3794                        this.last_external_paths_drag_over_entry = Some(entry_id);
3795                        this.marked_entries.clear();
3796
3797                        let Some((worktree, path, entry)) = maybe!({
3798                            let worktree = this
3799                                .project
3800                                .read(cx)
3801                                .worktree_for_id(selection.worktree_id, cx)?;
3802                            let worktree = worktree.read(cx);
3803                            let entry = worktree.entry_for_path(&path)?;
3804                            let path = if entry.is_dir() {
3805                                path.as_ref()
3806                            } else {
3807                                path.parent()?
3808                            };
3809                            Some((worktree, path, entry))
3810                        }) else {
3811                            return;
3812                        };
3813
3814                        this.marked_entries.insert(SelectedEntry {
3815                            entry_id: entry.id,
3816                            worktree_id: worktree.id(),
3817                        });
3818
3819                        for entry in worktree.child_entries(path) {
3820                            this.marked_entries.insert(SelectedEntry {
3821                                entry_id: entry.id,
3822                                worktree_id: worktree.id(),
3823                            });
3824                        }
3825
3826                        cx.notify();
3827                    }
3828                },
3829            ))
3830            .on_drop(cx.listener(
3831                move |this, external_paths: &ExternalPaths, window, cx| {
3832                    this.hover_scroll_task.take();
3833                    this.last_external_paths_drag_over_entry = None;
3834                    this.marked_entries.clear();
3835                    this.drop_external_files(external_paths.paths(), entry_id, window, cx);
3836                    cx.stop_propagation();
3837                },
3838            ))
3839            .on_drag_move::<DraggedSelection>(cx.listener(
3840                move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
3841                    if event.bounds.contains(&event.event.position) {
3842                        if this.last_selection_drag_over_entry == Some(entry_id) {
3843                            return;
3844                        }
3845                        this.last_selection_drag_over_entry = Some(entry_id);
3846                        this.hover_expand_task.take();
3847
3848                        if !kind.is_dir()
3849                            || this
3850                                .expanded_dir_ids
3851                                .get(&details.worktree_id)
3852                                .map_or(false, |ids| ids.binary_search(&entry_id).is_ok())
3853                        {
3854                            return;
3855                        }
3856
3857                        let bounds = event.bounds;
3858                        this.hover_expand_task =
3859                            Some(cx.spawn_in(window, async move |this, cx| {
3860                                cx.background_executor()
3861                                    .timer(Duration::from_millis(500))
3862                                    .await;
3863                                this.update_in(cx, |this, window, cx| {
3864                                    this.hover_expand_task.take();
3865                                    if this.last_selection_drag_over_entry == Some(entry_id)
3866                                        && bounds.contains(&window.mouse_position())
3867                                    {
3868                                        this.expand_entry(worktree_id, entry_id, cx);
3869                                        this.update_visible_entries(
3870                                            Some((worktree_id, entry_id)),
3871                                            cx,
3872                                        );
3873                                        cx.notify();
3874                                    }
3875                                })
3876                                .ok();
3877                            }));
3878                    }
3879                },
3880            ))
3881            .on_drag(
3882                dragged_selection,
3883                move |selection, click_offset, _window, cx| {
3884                    cx.new(|_| DraggedProjectEntryView {
3885                        details: details.clone(),
3886                        click_offset,
3887                        selection: selection.active_selection,
3888                        selections: selection.marked_selections.clone(),
3889                    })
3890                },
3891            )
3892            .drag_over::<DraggedSelection>(move |style, _, _, _| {
3893                if  folded_directory_drag_target.is_some() {
3894                    return style;
3895                }
3896                style.bg(item_colors.drag_over)
3897            })
3898            .on_drop(
3899                cx.listener(move |this, selections: &DraggedSelection, window, cx| {
3900                    this.hover_scroll_task.take();
3901                    this.hover_expand_task.take();
3902                    if  folded_directory_drag_target.is_some() {
3903                        return;
3904                    }
3905                    this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
3906                }),
3907            )
3908            .on_mouse_down(
3909                MouseButton::Left,
3910                cx.listener(move |this, _, _, cx| {
3911                    this.mouse_down = true;
3912                    cx.propagate();
3913                }),
3914            )
3915            .on_click(
3916                cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
3917                    if event.down.button == MouseButton::Right
3918                        || event.down.first_mouse
3919                        || show_editor
3920                    {
3921                        return;
3922                    }
3923                    if event.down.button == MouseButton::Left {
3924                        this.mouse_down = false;
3925                    }
3926                    cx.stop_propagation();
3927
3928                    if let Some(selection) = this.selection.filter(|_| event.modifiers().shift) {
3929                        let current_selection = this.index_for_selection(selection);
3930                        let clicked_entry = SelectedEntry {
3931                            entry_id,
3932                            worktree_id,
3933                        };
3934                        let target_selection = this.index_for_selection(clicked_entry);
3935                        if let Some(((_, _, source_index), (_, _, target_index))) =
3936                            current_selection.zip(target_selection)
3937                        {
3938                            let range_start = source_index.min(target_index);
3939                            let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
3940                            let mut new_selections = BTreeSet::new();
3941                            this.for_each_visible_entry(
3942                                range_start..range_end,
3943                                window,
3944                                cx,
3945                                |entry_id, details, _, _| {
3946                                    new_selections.insert(SelectedEntry {
3947                                        entry_id,
3948                                        worktree_id: details.worktree_id,
3949                                    });
3950                                },
3951                            );
3952
3953                            this.marked_entries = this
3954                                .marked_entries
3955                                .union(&new_selections)
3956                                .cloned()
3957                                .collect();
3958
3959                            this.selection = Some(clicked_entry);
3960                            this.marked_entries.insert(clicked_entry);
3961                        }
3962                    } else if event.modifiers().secondary() {
3963                        if event.down.click_count > 1 {
3964                            this.split_entry(entry_id, cx);
3965                        } else {
3966                            this.selection = Some(selection);
3967                            if !this.marked_entries.insert(selection) {
3968                                this.marked_entries.remove(&selection);
3969                            }
3970                        }
3971                    } else if kind.is_dir() {
3972                        this.marked_entries.clear();
3973                        if event.modifiers().alt {
3974                            this.toggle_expand_all(entry_id, window, cx);
3975                        } else {
3976                            this.toggle_expanded(entry_id, window, cx);
3977                        }
3978                    } else {
3979                        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
3980                        let click_count = event.up.click_count;
3981                        let focus_opened_item = !preview_tabs_enabled || click_count > 1;
3982                        let allow_preview = preview_tabs_enabled && click_count == 1;
3983                        this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
3984                    }
3985                }),
3986            )
3987            .child(
3988                ListItem::new(entry_id.to_proto() as usize)
3989                    .indent_level(depth)
3990                    .indent_step_size(px(settings.indent_size))
3991                    .spacing(match settings.entry_spacing {
3992                        project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense,
3993                        project_panel_settings::EntrySpacing::Standard => {
3994                            ListItemSpacing::ExtraDense
3995                        }
3996                    })
3997                    .selectable(false)
3998                    .when_some(canonical_path, |this, path| {
3999                        this.end_slot::<AnyElement>(
4000                            div()
4001                                .id("symlink_icon")
4002                                .pr_3()
4003                                .tooltip(move |window, cx| {
4004                                    Tooltip::with_meta(
4005                                        path.to_string(),
4006                                        None,
4007                                        "Symbolic Link",
4008                                        window,
4009                                        cx,
4010                                    )
4011                                })
4012                                .child(
4013                                    Icon::new(IconName::ArrowUpRight)
4014                                        .size(IconSize::Indicator)
4015                                        .color(filename_text_color),
4016                                )
4017                                .into_any_element(),
4018                        )
4019                    })
4020                    .child(if let Some(icon) = &icon {
4021                        if let Some((_, decoration_color)) =
4022                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
4023                        {
4024                            let is_warning = diagnostic_severity
4025                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
4026                                .unwrap_or(false);
4027                            div().child(
4028                                DecoratedIcon::new(
4029                                    Icon::from_path(icon.clone()).color(Color::Muted),
4030                                    Some(
4031                                        IconDecoration::new(
4032                                            if kind.is_file() {
4033                                                if is_warning {
4034                                                    IconDecorationKind::Triangle
4035                                                } else {
4036                                                    IconDecorationKind::X
4037                                                }
4038                                            } else {
4039                                                IconDecorationKind::Dot
4040                                            },
4041                                            bg_color,
4042                                            cx,
4043                                        )
4044                                        .group_name(Some(GROUP_NAME.into()))
4045                                        .knockout_hover_color(bg_hover_color)
4046                                        .color(decoration_color.color(cx))
4047                                        .position(Point {
4048                                            x: px(-2.),
4049                                            y: px(-2.),
4050                                        }),
4051                                    ),
4052                                )
4053                                .into_any_element(),
4054                            )
4055                        } else {
4056                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
4057                        }
4058                    } else {
4059                        if let Some((icon_name, color)) =
4060                            entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
4061                        {
4062                            h_flex()
4063                                .size(IconSize::default().rems())
4064                                .child(Icon::new(icon_name).color(color).size(IconSize::Small))
4065                        } else {
4066                            h_flex()
4067                                .size(IconSize::default().rems())
4068                                .invisible()
4069                                .flex_none()
4070                        }
4071                    })
4072                    .child(
4073                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
4074                            h_flex().h_6().w_full().child(editor.clone())
4075                        } else {
4076                            h_flex().h_6().map(|mut this| {
4077                                if let Some(folded_ancestors) = self.ancestors.get(&entry_id) {
4078                                    let components = Path::new(&file_name)
4079                                        .components()
4080                                        .map(|comp| {
4081                                            let comp_str =
4082                                                comp.as_os_str().to_string_lossy().into_owned();
4083                                            comp_str
4084                                        })
4085                                        .collect::<Vec<_>>();
4086
4087                                    let components_len = components.len();
4088                                    let active_index = components_len
4089                                        - 1
4090                                        - folded_ancestors.current_ancestor_depth;
4091                                        const DELIMITER: SharedString =
4092                                        SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
4093                                    for (index, component) in components.into_iter().enumerate() {
4094                                        if index != 0 {
4095                                                let delimiter_target_index = index - 1;
4096                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
4097                                                this = this.child(
4098                                                    div()
4099                                                    .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
4100                                                        this.hover_scroll_task.take();
4101                                                        this.folded_directory_drag_target = None;
4102                                                        if let Some(target_entry_id) = target_entry_id {
4103                                                            this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4104                                                        }
4105                                                    }))
4106                                                    .on_drag_move(cx.listener(
4107                                                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4108                                                            if event.bounds.contains(&event.event.position) {
4109                                                                this.folded_directory_drag_target = Some(
4110                                                                    FoldedDirectoryDragTarget {
4111                                                                        entry_id,
4112                                                                        index: delimiter_target_index,
4113                                                                        is_delimiter_target: true,
4114                                                                    }
4115                                                                );
4116                                                            } else {
4117                                                                let is_current_target = this.folded_directory_drag_target
4118                                                                    .map_or(false, |target|
4119                                                                        target.entry_id == entry_id &&
4120                                                                        target.index == delimiter_target_index &&
4121                                                                        target.is_delimiter_target
4122                                                                    );
4123                                                                if is_current_target {
4124                                                                    this.folded_directory_drag_target = None;
4125                                                                }
4126                                                            }
4127
4128                                                        },
4129                                                    ))
4130                                                    .child(
4131                                                        Label::new(DELIMITER.clone())
4132                                                            .single_line()
4133                                                            .color(filename_text_color)
4134                                                    )
4135                                                );
4136                                        }
4137                                        let id = SharedString::from(format!(
4138                                            "project_panel_path_component_{}_{index}",
4139                                            entry_id.to_usize()
4140                                        ));
4141                                        let label = div()
4142                                            .id(id)
4143                                            .on_click(cx.listener(move |this, _, _, cx| {
4144                                                if index != active_index {
4145                                                    if let Some(folds) =
4146                                                        this.ancestors.get_mut(&entry_id)
4147                                                    {
4148                                                        folds.current_ancestor_depth =
4149                                                            components_len - 1 - index;
4150                                                        cx.notify();
4151                                                    }
4152                                                }
4153                                            }))
4154                                            .when(index != components_len - 1, |div|{
4155                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
4156                                                div
4157                                                .on_drag_move(cx.listener(
4158                                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4159                                                    if event.bounds.contains(&event.event.position) {
4160                                                            this.folded_directory_drag_target = Some(
4161                                                                FoldedDirectoryDragTarget {
4162                                                                    entry_id,
4163                                                                    index,
4164                                                                    is_delimiter_target: false,
4165                                                                }
4166                                                            );
4167                                                        } else {
4168                                                            let is_current_target = this.folded_directory_drag_target
4169                                                                .as_ref()
4170                                                                .map_or(false, |target|
4171                                                                    target.entry_id == entry_id &&
4172                                                                    target.index == index &&
4173                                                                    !target.is_delimiter_target
4174                                                                );
4175                                                            if is_current_target {
4176                                                                this.folded_directory_drag_target = None;
4177                                                            }
4178                                                        }
4179                                                    },
4180                                                ))
4181                                                .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| {
4182                                                    this.hover_scroll_task.take();
4183                                                    this.folded_directory_drag_target = None;
4184                                                    if let Some(target_entry_id) = target_entry_id {
4185                                                        this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4186                                                    }
4187                                                }))
4188                                                .when(folded_directory_drag_target.map_or(false, |target|
4189                                                    target.entry_id == entry_id &&
4190                                                    target.index == index
4191                                                ), |this| {
4192                                                    this.bg(item_colors.drag_over)
4193                                                })
4194                                            })
4195                                            .child(
4196                                                Label::new(component)
4197                                                    .single_line()
4198                                                    .color(filename_text_color)
4199                                                    .when(
4200                                                        index == active_index
4201                                                            && (is_active || is_marked),
4202                                                        |this| this.underline(),
4203                                                    ),
4204                                            );
4205
4206                                        this = this.child(label);
4207                                    }
4208
4209                                    this
4210                                } else {
4211                                    this.child(
4212                                        Label::new(file_name)
4213                                            .single_line()
4214                                            .color(filename_text_color),
4215                                    )
4216                                }
4217                            })
4218                        }
4219                        .ml_1(),
4220                    )
4221                    .on_secondary_mouse_down(cx.listener(
4222                        move |this, event: &MouseDownEvent, window, cx| {
4223                            // Stop propagation to prevent the catch-all context menu for the project
4224                            // panel from being deployed.
4225                            cx.stop_propagation();
4226                            // Some context menu actions apply to all marked entries. If the user
4227                            // right-clicks on an entry that is not marked, they may not realize the
4228                            // action applies to multiple entries. To avoid inadvertent changes, all
4229                            // entries are unmarked.
4230                            if !this.marked_entries.contains(&selection) {
4231                                this.marked_entries.clear();
4232                            }
4233                            this.deploy_context_menu(event.position, entry_id, window, cx);
4234                        },
4235                    ))
4236                    .overflow_x(),
4237            )
4238            .when_some(
4239                validation_color_and_message,
4240                |this, (color, message)| {
4241                    this
4242                    .relative()
4243                    .child(
4244                        deferred(
4245                            div()
4246                            .occlude()
4247                            .absolute()
4248                            .top_full()
4249                            .left(px(-1.)) // Used px over rem so that it doesn't change with font size
4250                            .right(px(-0.5))
4251                            .py_1()
4252                            .px_2()
4253                            .border_1()
4254                            .border_color(color)
4255                            .bg(cx.theme().colors().background)
4256                            .child(
4257                                Label::new(message)
4258                                .color(Color::from(color))
4259                                .size(LabelSize::Small)
4260                            )
4261                        )
4262                    )
4263                }
4264            )
4265    }
4266
4267    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4268        if !Self::should_show_scrollbar(cx)
4269            || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4270        {
4271            return None;
4272        }
4273        Some(
4274            div()
4275                .occlude()
4276                .id("project-panel-vertical-scroll")
4277                .on_mouse_move(cx.listener(|_, _, _, cx| {
4278                    cx.notify();
4279                    cx.stop_propagation()
4280                }))
4281                .on_hover(|_, _, cx| {
4282                    cx.stop_propagation();
4283                })
4284                .on_any_mouse_down(|_, _, cx| {
4285                    cx.stop_propagation();
4286                })
4287                .on_mouse_up(
4288                    MouseButton::Left,
4289                    cx.listener(|this, _, window, cx| {
4290                        if !this.vertical_scrollbar_state.is_dragging()
4291                            && !this.focus_handle.contains_focused(window, cx)
4292                        {
4293                            this.hide_scrollbar(window, cx);
4294                            cx.notify();
4295                        }
4296
4297                        cx.stop_propagation();
4298                    }),
4299                )
4300                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4301                    cx.notify();
4302                }))
4303                .h_full()
4304                .absolute()
4305                .right_1()
4306                .top_1()
4307                .bottom_1()
4308                .w(px(12.))
4309                .cursor_default()
4310                .children(Scrollbar::vertical(
4311                    // percentage as f32..end_offset as f32,
4312                    self.vertical_scrollbar_state.clone(),
4313                )),
4314        )
4315    }
4316
4317    fn render_horizontal_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4318        if !Self::should_show_scrollbar(cx)
4319            || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4320        {
4321            return None;
4322        }
4323
4324        let scroll_handle = self.scroll_handle.0.borrow();
4325        let longest_item_width = scroll_handle
4326            .last_item_size
4327            .filter(|size| size.contents.width > size.item.width)?
4328            .contents
4329            .width
4330            .0 as f64;
4331        if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
4332            return None;
4333        }
4334
4335        Some(
4336            div()
4337                .occlude()
4338                .id("project-panel-horizontal-scroll")
4339                .on_mouse_move(cx.listener(|_, _, _, cx| {
4340                    cx.notify();
4341                    cx.stop_propagation()
4342                }))
4343                .on_hover(|_, _, cx| {
4344                    cx.stop_propagation();
4345                })
4346                .on_any_mouse_down(|_, _, cx| {
4347                    cx.stop_propagation();
4348                })
4349                .on_mouse_up(
4350                    MouseButton::Left,
4351                    cx.listener(|this, _, window, cx| {
4352                        if !this.horizontal_scrollbar_state.is_dragging()
4353                            && !this.focus_handle.contains_focused(window, cx)
4354                        {
4355                            this.hide_scrollbar(window, cx);
4356                            cx.notify();
4357                        }
4358
4359                        cx.stop_propagation();
4360                    }),
4361                )
4362                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4363                    cx.notify();
4364                }))
4365                .w_full()
4366                .absolute()
4367                .right_1()
4368                .left_1()
4369                .bottom_1()
4370                .h(px(12.))
4371                .cursor_default()
4372                .when(self.width.is_some(), |this| {
4373                    this.children(Scrollbar::horizontal(
4374                        self.horizontal_scrollbar_state.clone(),
4375                    ))
4376                }),
4377        )
4378    }
4379
4380    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
4381        let mut dispatch_context = KeyContext::new_with_defaults();
4382        dispatch_context.add("ProjectPanel");
4383        dispatch_context.add("menu");
4384
4385        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
4386            "editing"
4387        } else {
4388            "not_editing"
4389        };
4390
4391        dispatch_context.add(identifier);
4392        dispatch_context
4393    }
4394
4395    fn should_show_scrollbar(cx: &App) -> bool {
4396        let show = ProjectPanelSettings::get_global(cx)
4397            .scrollbar
4398            .show
4399            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4400        match show {
4401            ShowScrollbar::Auto => true,
4402            ShowScrollbar::System => true,
4403            ShowScrollbar::Always => true,
4404            ShowScrollbar::Never => false,
4405        }
4406    }
4407
4408    fn should_autohide_scrollbar(cx: &App) -> bool {
4409        let show = ProjectPanelSettings::get_global(cx)
4410            .scrollbar
4411            .show
4412            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4413        match show {
4414            ShowScrollbar::Auto => true,
4415            ShowScrollbar::System => cx
4416                .try_global::<ScrollbarAutoHide>()
4417                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4418            ShowScrollbar::Always => false,
4419            ShowScrollbar::Never => true,
4420        }
4421    }
4422
4423    fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4424        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4425        if !Self::should_autohide_scrollbar(cx) {
4426            return;
4427        }
4428        self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4429            cx.background_executor()
4430                .timer(SCROLLBAR_SHOW_INTERVAL)
4431                .await;
4432            panel
4433                .update(cx, |panel, cx| {
4434                    panel.show_scrollbar = false;
4435                    cx.notify();
4436                })
4437                .log_err();
4438        }))
4439    }
4440
4441    fn reveal_entry(
4442        &mut self,
4443        project: Entity<Project>,
4444        entry_id: ProjectEntryId,
4445        skip_ignored: bool,
4446        cx: &mut Context<Self>,
4447    ) -> Result<()> {
4448        if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
4449            let worktree = worktree.read(cx);
4450            if skip_ignored
4451                && worktree
4452                    .entry_for_id(entry_id)
4453                    .map_or(true, |entry| entry.is_ignored && !entry.is_always_included)
4454            {
4455                return Err(anyhow!(
4456                    "can't reveal an ignored entry in the project panel"
4457                ));
4458            }
4459
4460            let worktree_id = worktree.id();
4461            self.expand_entry(worktree_id, entry_id, cx);
4462            self.update_visible_entries(Some((worktree_id, entry_id)), cx);
4463            self.marked_entries.clear();
4464            self.marked_entries.insert(SelectedEntry {
4465                worktree_id,
4466                entry_id,
4467            });
4468            self.autoscroll(cx);
4469            cx.notify();
4470            Ok(())
4471        } else {
4472            Err(anyhow!(
4473                "can't reveal a non-existent entry in the project panel"
4474            ))
4475        }
4476    }
4477
4478    fn find_active_indent_guide(
4479        &self,
4480        indent_guides: &[IndentGuideLayout],
4481        cx: &App,
4482    ) -> Option<usize> {
4483        let (worktree, entry) = self.selected_entry(cx)?;
4484
4485        // Find the parent entry of the indent guide, this will either be the
4486        // expanded folder we have selected, or the parent of the currently
4487        // selected file/collapsed directory
4488        let mut entry = entry;
4489        loop {
4490            let is_expanded_dir = entry.is_dir()
4491                && self
4492                    .expanded_dir_ids
4493                    .get(&worktree.id())
4494                    .map(|ids| ids.binary_search(&entry.id).is_ok())
4495                    .unwrap_or(false);
4496            if is_expanded_dir {
4497                break;
4498            }
4499            entry = worktree.entry_for_path(&entry.path.parent()?)?;
4500        }
4501
4502        let (active_indent_range, depth) = {
4503            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
4504            let child_paths = &self.visible_entries[worktree_ix].1;
4505            let mut child_count = 0;
4506            let depth = entry.path.ancestors().count();
4507            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
4508                if entry.path.ancestors().count() <= depth {
4509                    break;
4510                }
4511                child_count += 1;
4512            }
4513
4514            let start = ix + 1;
4515            let end = start + child_count;
4516
4517            let (_, entries, paths) = &self.visible_entries[worktree_ix];
4518            let visible_worktree_entries =
4519                paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect());
4520
4521            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
4522            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
4523            (start..end, depth)
4524        };
4525
4526        let candidates = indent_guides
4527            .iter()
4528            .enumerate()
4529            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
4530
4531        for (i, indent) in candidates {
4532            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
4533            if active_indent_range.start <= indent.offset.y + indent.length
4534                && indent.offset.y <= active_indent_range.end
4535            {
4536                return Some(i);
4537            }
4538        }
4539        None
4540    }
4541}
4542
4543fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
4544    const ICON_SIZE_FACTOR: usize = 2;
4545    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
4546    if is_symlink {
4547        item_width += ICON_SIZE_FACTOR;
4548    }
4549    item_width
4550}
4551
4552impl Render for ProjectPanel {
4553    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4554        let has_worktree = !self.visible_entries.is_empty();
4555        let project = self.project.read(cx);
4556        let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
4557        let show_indent_guides =
4558            ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
4559        let is_local = project.is_local();
4560
4561        if has_worktree {
4562            let item_count = self
4563                .visible_entries
4564                .iter()
4565                .map(|(_, worktree_entries, _)| worktree_entries.len())
4566                .sum();
4567
4568            fn handle_drag_move_scroll<T: 'static>(
4569                this: &mut ProjectPanel,
4570                e: &DragMoveEvent<T>,
4571                window: &mut Window,
4572                cx: &mut Context<ProjectPanel>,
4573            ) {
4574                if !e.bounds.contains(&e.event.position) {
4575                    return;
4576                }
4577                this.hover_scroll_task.take();
4578                let panel_height = e.bounds.size.height;
4579                if panel_height <= px(0.) {
4580                    return;
4581                }
4582
4583                let event_offset = e.event.position.y - e.bounds.origin.y;
4584                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
4585                let hovered_region_offset = event_offset / panel_height;
4586
4587                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
4588                // These pixels offsets were picked arbitrarily.
4589                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
4590                    8.
4591                } else if hovered_region_offset <= 0.15 {
4592                    5.
4593                } else if hovered_region_offset >= 0.95 {
4594                    -8.
4595                } else if hovered_region_offset >= 0.85 {
4596                    -5.
4597                } else {
4598                    return;
4599                };
4600                let adjustment = point(px(0.), px(vertical_scroll_offset));
4601                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
4602                    loop {
4603                        let should_stop_scrolling = this
4604                            .update(cx, |this, cx| {
4605                                this.hover_scroll_task.as_ref()?;
4606                                let handle = this.scroll_handle.0.borrow_mut();
4607                                let offset = handle.base_handle.offset();
4608
4609                                handle.base_handle.set_offset(offset + adjustment);
4610                                cx.notify();
4611                                Some(())
4612                            })
4613                            .ok()
4614                            .flatten()
4615                            .is_some();
4616                        if should_stop_scrolling {
4617                            return;
4618                        }
4619                        cx.background_executor()
4620                            .timer(Duration::from_millis(16))
4621                            .await;
4622                    }
4623                }));
4624            }
4625            h_flex()
4626                .id("project-panel")
4627                .group("project-panel")
4628                .on_drag_move(cx.listener(handle_drag_move_scroll::<ExternalPaths>))
4629                .on_drag_move(cx.listener(handle_drag_move_scroll::<DraggedSelection>))
4630                .size_full()
4631                .relative()
4632                .on_hover(cx.listener(|this, hovered, window, cx| {
4633                    if *hovered {
4634                        this.show_scrollbar = true;
4635                        this.hide_scrollbar_task.take();
4636                        cx.notify();
4637                    } else if !this.focus_handle.contains_focused(window, cx) {
4638                        this.hide_scrollbar(window, cx);
4639                    }
4640                }))
4641                .on_click(cx.listener(|this, _event, _, cx| {
4642                    cx.stop_propagation();
4643                    this.selection = None;
4644                    this.marked_entries.clear();
4645                }))
4646                .key_context(self.dispatch_context(window, cx))
4647                .on_action(cx.listener(Self::select_next))
4648                .on_action(cx.listener(Self::select_previous))
4649                .on_action(cx.listener(Self::select_first))
4650                .on_action(cx.listener(Self::select_last))
4651                .on_action(cx.listener(Self::select_parent))
4652                .on_action(cx.listener(Self::select_next_git_entry))
4653                .on_action(cx.listener(Self::select_prev_git_entry))
4654                .on_action(cx.listener(Self::select_next_diagnostic))
4655                .on_action(cx.listener(Self::select_prev_diagnostic))
4656                .on_action(cx.listener(Self::select_next_directory))
4657                .on_action(cx.listener(Self::select_prev_directory))
4658                .on_action(cx.listener(Self::expand_selected_entry))
4659                .on_action(cx.listener(Self::collapse_selected_entry))
4660                .on_action(cx.listener(Self::collapse_all_entries))
4661                .on_action(cx.listener(Self::open))
4662                .on_action(cx.listener(Self::open_permanent))
4663                .on_action(cx.listener(Self::confirm))
4664                .on_action(cx.listener(Self::cancel))
4665                .on_action(cx.listener(Self::copy_path))
4666                .on_action(cx.listener(Self::copy_relative_path))
4667                .on_action(cx.listener(Self::new_search_in_directory))
4668                .on_action(cx.listener(Self::unfold_directory))
4669                .on_action(cx.listener(Self::fold_directory))
4670                .on_action(cx.listener(Self::remove_from_project))
4671                .when(!project.is_read_only(cx), |el| {
4672                    el.on_action(cx.listener(Self::new_file))
4673                        .on_action(cx.listener(Self::new_directory))
4674                        .on_action(cx.listener(Self::rename))
4675                        .on_action(cx.listener(Self::delete))
4676                        .on_action(cx.listener(Self::trash))
4677                        .on_action(cx.listener(Self::cut))
4678                        .on_action(cx.listener(Self::copy))
4679                        .on_action(cx.listener(Self::paste))
4680                        .on_action(cx.listener(Self::duplicate))
4681                        .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| {
4682                            if event.up.click_count > 1 {
4683                                if let Some(entry_id) = this.last_worktree_root_id {
4684                                    let project = this.project.read(cx);
4685
4686                                    let worktree_id = if let Some(worktree) =
4687                                        project.worktree_for_entry(entry_id, cx)
4688                                    {
4689                                        worktree.read(cx).id()
4690                                    } else {
4691                                        return;
4692                                    };
4693
4694                                    this.selection = Some(SelectedEntry {
4695                                        worktree_id,
4696                                        entry_id,
4697                                    });
4698
4699                                    this.new_file(&NewFile, window, cx);
4700                                }
4701                            }
4702                        }))
4703                })
4704                .when(project.is_local(), |el| {
4705                    el.on_action(cx.listener(Self::reveal_in_finder))
4706                        .on_action(cx.listener(Self::open_system))
4707                        .on_action(cx.listener(Self::open_in_terminal))
4708                })
4709                .when(project.is_via_ssh(), |el| {
4710                    el.on_action(cx.listener(Self::open_in_terminal))
4711                })
4712                .on_mouse_down(
4713                    MouseButton::Right,
4714                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
4715                        // When deploying the context menu anywhere below the last project entry,
4716                        // act as if the user clicked the root of the last worktree.
4717                        if let Some(entry_id) = this.last_worktree_root_id {
4718                            this.deploy_context_menu(event.position, entry_id, window, cx);
4719                        }
4720                    }),
4721                )
4722                .track_focus(&self.focus_handle(cx))
4723                .child(
4724                    uniform_list(cx.entity().clone(), "entries", item_count, {
4725                        |this, range, window, cx| {
4726                            let mut items = Vec::with_capacity(range.end - range.start);
4727                            this.for_each_visible_entry(
4728                                range,
4729                                window,
4730                                cx,
4731                                |id, details, window, cx| {
4732                                    items.push(this.render_entry(id, details, window, cx));
4733                                },
4734                            );
4735                            items
4736                        }
4737                    })
4738                    .when(show_indent_guides, |list| {
4739                        list.with_decoration(
4740                            ui::indent_guides(
4741                                cx.entity().clone(),
4742                                px(indent_size),
4743                                IndentGuideColors::panel(cx),
4744                                |this, range, window, cx| {
4745                                    let mut items =
4746                                        SmallVec::with_capacity(range.end - range.start);
4747                                    this.iter_visible_entries(
4748                                        range,
4749                                        window,
4750                                        cx,
4751                                        |entry, entries, _, _| {
4752                                            let (depth, _) = Self::calculate_depth_and_difference(
4753                                                entry, entries,
4754                                            );
4755                                            items.push(depth);
4756                                        },
4757                                    );
4758                                    items
4759                                },
4760                            )
4761                            .on_click(cx.listener(
4762                                |this, active_indent_guide: &IndentGuideLayout, window, cx| {
4763                                    if window.modifiers().secondary() {
4764                                        let ix = active_indent_guide.offset.y;
4765                                        let Some((target_entry, worktree)) = maybe!({
4766                                            let (worktree_id, entry) = this.entry_at_index(ix)?;
4767                                            let worktree = this
4768                                                .project
4769                                                .read(cx)
4770                                                .worktree_for_id(worktree_id, cx)?;
4771                                            let target_entry = worktree
4772                                                .read(cx)
4773                                                .entry_for_path(&entry.path.parent()?)?;
4774                                            Some((target_entry, worktree))
4775                                        }) else {
4776                                            return;
4777                                        };
4778
4779                                        this.collapse_entry(target_entry.clone(), worktree, cx);
4780                                    }
4781                                },
4782                            ))
4783                            .with_render_fn(
4784                                cx.entity().clone(),
4785                                move |this, params, _, cx| {
4786                                    const LEFT_OFFSET: Pixels = px(14.);
4787                                    const PADDING_Y: Pixels = px(4.);
4788                                    const HITBOX_OVERDRAW: Pixels = px(3.);
4789
4790                                    let active_indent_guide_index =
4791                                        this.find_active_indent_guide(&params.indent_guides, cx);
4792
4793                                    let indent_size = params.indent_size;
4794                                    let item_height = params.item_height;
4795
4796                                    params
4797                                        .indent_guides
4798                                        .into_iter()
4799                                        .enumerate()
4800                                        .map(|(idx, layout)| {
4801                                            let offset = if layout.continues_offscreen {
4802                                                px(0.)
4803                                            } else {
4804                                                PADDING_Y
4805                                            };
4806                                            let bounds = Bounds::new(
4807                                                point(
4808                                                    layout.offset.x * indent_size + LEFT_OFFSET,
4809                                                    layout.offset.y * item_height + offset,
4810                                                ),
4811                                                size(
4812                                                    px(1.),
4813                                                    layout.length * item_height - offset * 2.,
4814                                                ),
4815                                            );
4816                                            ui::RenderedIndentGuide {
4817                                                bounds,
4818                                                layout,
4819                                                is_active: Some(idx) == active_indent_guide_index,
4820                                                hitbox: Some(Bounds::new(
4821                                                    point(
4822                                                        bounds.origin.x - HITBOX_OVERDRAW,
4823                                                        bounds.origin.y,
4824                                                    ),
4825                                                    size(
4826                                                        bounds.size.width + HITBOX_OVERDRAW * 2.,
4827                                                        bounds.size.height,
4828                                                    ),
4829                                                )),
4830                                            }
4831                                        })
4832                                        .collect()
4833                                },
4834                            ),
4835                        )
4836                    })
4837                    .size_full()
4838                    .with_sizing_behavior(ListSizingBehavior::Infer)
4839                    .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4840                    .with_width_from_item(self.max_width_item_index)
4841                    .track_scroll(self.scroll_handle.clone()),
4842                )
4843                .children(self.render_vertical_scrollbar(cx))
4844                .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
4845                    this.pb_4().child(scrollbar)
4846                })
4847                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4848                    deferred(
4849                        anchored()
4850                            .position(*position)
4851                            .anchor(gpui::Corner::TopLeft)
4852                            .child(menu.clone()),
4853                    )
4854                    .with_priority(1)
4855                }))
4856        } else {
4857            v_flex()
4858                .id("empty-project_panel")
4859                .size_full()
4860                .p_4()
4861                .track_focus(&self.focus_handle(cx))
4862                .child(
4863                    Button::new("open_project", "Open a project")
4864                        .full_width()
4865                        .key_binding(KeyBinding::for_action(&workspace::Open, window, cx))
4866                        .on_click(cx.listener(|this, _, window, cx| {
4867                            this.workspace
4868                                .update(cx, |_, cx| {
4869                                    window.dispatch_action(Box::new(workspace::Open), cx)
4870                                })
4871                                .log_err();
4872                        })),
4873                )
4874                .when(is_local, |div| {
4875                    div.drag_over::<ExternalPaths>(|style, _, _, cx| {
4876                        style.bg(cx.theme().colors().drop_target_background)
4877                    })
4878                    .on_drop(cx.listener(
4879                        move |this, external_paths: &ExternalPaths, window, cx| {
4880                            this.last_external_paths_drag_over_entry = None;
4881                            this.marked_entries.clear();
4882                            this.hover_scroll_task.take();
4883                            if let Some(task) = this
4884                                .workspace
4885                                .update(cx, |workspace, cx| {
4886                                    workspace.open_workspace_for_paths(
4887                                        true,
4888                                        external_paths.paths().to_owned(),
4889                                        window,
4890                                        cx,
4891                                    )
4892                                })
4893                                .log_err()
4894                            {
4895                                task.detach_and_log_err(cx);
4896                            }
4897                            cx.stop_propagation();
4898                        },
4899                    ))
4900                })
4901        }
4902    }
4903}
4904
4905impl Render for DraggedProjectEntryView {
4906    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4907        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
4908        h_flex()
4909            .font(ui_font)
4910            .pl(self.click_offset.x + px(12.))
4911            .pt(self.click_offset.y + px(12.))
4912            .child(
4913                div()
4914                    .flex()
4915                    .gap_1()
4916                    .items_center()
4917                    .py_1()
4918                    .px_2()
4919                    .rounded_lg()
4920                    .bg(cx.theme().colors().background)
4921                    .map(|this| {
4922                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
4923                            this.child(Label::new(format!("{} entries", self.selections.len())))
4924                        } else {
4925                            this.child(if let Some(icon) = &self.details.icon {
4926                                div().child(Icon::from_path(icon.clone()))
4927                            } else {
4928                                div()
4929                            })
4930                            .child(Label::new(self.details.filename.clone()))
4931                        }
4932                    }),
4933            )
4934    }
4935}
4936
4937impl EventEmitter<Event> for ProjectPanel {}
4938
4939impl EventEmitter<PanelEvent> for ProjectPanel {}
4940
4941impl Panel for ProjectPanel {
4942    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4943        match ProjectPanelSettings::get_global(cx).dock {
4944            ProjectPanelDockPosition::Left => DockPosition::Left,
4945            ProjectPanelDockPosition::Right => DockPosition::Right,
4946        }
4947    }
4948
4949    fn position_is_valid(&self, position: DockPosition) -> bool {
4950        matches!(position, DockPosition::Left | DockPosition::Right)
4951    }
4952
4953    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4954        settings::update_settings_file::<ProjectPanelSettings>(
4955            self.fs.clone(),
4956            cx,
4957            move |settings, _| {
4958                let dock = match position {
4959                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
4960                    DockPosition::Right => ProjectPanelDockPosition::Right,
4961                };
4962                settings.dock = Some(dock);
4963            },
4964        );
4965    }
4966
4967    fn size(&self, _: &Window, cx: &App) -> Pixels {
4968        self.width
4969            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
4970    }
4971
4972    fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
4973        self.width = size;
4974        self.serialize(cx);
4975        cx.notify();
4976    }
4977
4978    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4979        ProjectPanelSettings::get_global(cx)
4980            .button
4981            .then_some(IconName::FileTree)
4982    }
4983
4984    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
4985        Some("Project Panel")
4986    }
4987
4988    fn toggle_action(&self) -> Box<dyn Action> {
4989        Box::new(ToggleFocus)
4990    }
4991
4992    fn persistent_name() -> &'static str {
4993        "Project Panel"
4994    }
4995
4996    fn starts_open(&self, _: &Window, cx: &App) -> bool {
4997        let project = &self.project.read(cx);
4998        project.visible_worktrees(cx).any(|tree| {
4999            tree.read(cx)
5000                .root_entry()
5001                .map_or(false, |entry| entry.is_dir())
5002        })
5003    }
5004
5005    fn activation_priority(&self) -> u32 {
5006        0
5007    }
5008}
5009
5010impl Focusable for ProjectPanel {
5011    fn focus_handle(&self, _cx: &App) -> FocusHandle {
5012        self.focus_handle.clone()
5013    }
5014}
5015
5016impl ClipboardEntry {
5017    fn is_cut(&self) -> bool {
5018        matches!(self, Self::Cut { .. })
5019    }
5020
5021    fn items(&self) -> &BTreeSet<SelectedEntry> {
5022        match self {
5023            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
5024        }
5025    }
5026}
5027
5028#[cfg(test)]
5029mod project_panel_tests;