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.file_name() else {
2859                        continue;
2860                    };
2861                    let path = ArcCow::Borrowed(Path::new(path_name));
2862                    let depth = 0;
2863                    (depth, path)
2864                } else if entry.is_file() {
2865                    let Some(path_name) = entry
2866                        .path
2867                        .file_name()
2868                        .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
2869                        .log_err()
2870                    else {
2871                        continue;
2872                    };
2873                    let path = ArcCow::Borrowed(Path::new(path_name));
2874                    let depth = entry.path.ancestors().count() - 1;
2875                    (depth, path)
2876                } else {
2877                    let path = self
2878                        .ancestors
2879                        .get(&entry.id)
2880                        .and_then(|ancestors| {
2881                            let outermost_ancestor = ancestors.ancestors.last()?;
2882                            let root_folded_entry = worktree
2883                                .read(cx)
2884                                .entry_for_id(*outermost_ancestor)?
2885                                .path
2886                                .as_ref();
2887                            entry
2888                                .path
2889                                .strip_prefix(root_folded_entry)
2890                                .ok()
2891                                .and_then(|suffix| {
2892                                    let full_path = Path::new(root_folded_entry.file_name()?);
2893                                    Some(ArcCow::Owned(Arc::<Path>::from(full_path.join(suffix))))
2894                                })
2895                        })
2896                        .or_else(|| entry.path.file_name().map(Path::new).map(ArcCow::Borrowed))
2897                        .unwrap_or_else(|| ArcCow::Owned(entry.path.clone()));
2898                    let depth = path.components().count();
2899                    (depth, path)
2900                };
2901                let width_estimate = item_width_estimate(
2902                    depth,
2903                    path.to_string_lossy().chars().count(),
2904                    entry.canonical_path.is_some(),
2905                );
2906
2907                match max_width_item.as_mut() {
2908                    Some((id, worktree_id, width)) => {
2909                        if *width < width_estimate {
2910                            *id = entry.id;
2911                            *worktree_id = worktree.read(cx).id();
2912                            *width = width_estimate;
2913                        }
2914                    }
2915                    None => {
2916                        max_width_item = Some((entry.id, worktree.read(cx).id(), width_estimate))
2917                    }
2918                }
2919
2920                if expanded_dir_ids.binary_search(&entry.id).is_err()
2921                    && entry_iter.advance_to_sibling()
2922                {
2923                    continue;
2924                }
2925                entry_iter.advance();
2926            }
2927
2928            project::sort_worktree_entries(&mut visible_worktree_entries);
2929
2930            self.visible_entries
2931                .push((worktree_id, visible_worktree_entries, OnceCell::new()));
2932        }
2933
2934        if let Some((project_entry_id, worktree_id, _)) = max_width_item {
2935            let mut visited_worktrees_length = 0;
2936            let index = self.visible_entries.iter().find_map(|(id, entries, _)| {
2937                if worktree_id == *id {
2938                    entries
2939                        .iter()
2940                        .position(|entry| entry.id == project_entry_id)
2941                } else {
2942                    visited_worktrees_length += entries.len();
2943                    None
2944                }
2945            });
2946            if let Some(index) = index {
2947                self.max_width_item_index = Some(visited_worktrees_length + index);
2948            }
2949        }
2950        if let Some((worktree_id, entry_id)) = new_selected_entry {
2951            self.selection = Some(SelectedEntry {
2952                worktree_id,
2953                entry_id,
2954            });
2955        }
2956    }
2957
2958    fn expand_entry(
2959        &mut self,
2960        worktree_id: WorktreeId,
2961        entry_id: ProjectEntryId,
2962        cx: &mut Context<Self>,
2963    ) {
2964        self.project.update(cx, |project, cx| {
2965            if let Some((worktree, expanded_dir_ids)) = project
2966                .worktree_for_id(worktree_id, cx)
2967                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
2968            {
2969                project.expand_entry(worktree_id, entry_id, cx);
2970                let worktree = worktree.read(cx);
2971
2972                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
2973                    loop {
2974                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
2975                            expanded_dir_ids.insert(ix, entry.id);
2976                        }
2977
2978                        if let Some(parent_entry) =
2979                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
2980                        {
2981                            entry = parent_entry;
2982                        } else {
2983                            break;
2984                        }
2985                    }
2986                }
2987            }
2988        });
2989    }
2990
2991    fn drop_external_files(
2992        &mut self,
2993        paths: &[PathBuf],
2994        entry_id: ProjectEntryId,
2995        window: &mut Window,
2996        cx: &mut Context<Self>,
2997    ) {
2998        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
2999
3000        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
3001
3002        let Some((target_directory, worktree, fs)) = maybe!({
3003            let project = self.project.read(cx);
3004            let fs = project.fs().clone();
3005            let worktree = project.worktree_for_entry(entry_id, cx)?;
3006            let entry = worktree.read(cx).entry_for_id(entry_id)?;
3007            let path = entry.path.clone();
3008            let target_directory = if entry.is_dir() {
3009                path.to_path_buf()
3010            } else {
3011                path.parent()?.to_path_buf()
3012            };
3013            Some((target_directory, worktree, fs))
3014        }) else {
3015            return;
3016        };
3017
3018        let mut paths_to_replace = Vec::new();
3019        for path in &paths {
3020            if let Some(name) = path.file_name() {
3021                let mut target_path = target_directory.clone();
3022                target_path.push(name);
3023                if target_path.exists() {
3024                    paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
3025                }
3026            }
3027        }
3028
3029        cx.spawn_in(window, async move |this, cx| {
3030            async move {
3031                for (filename, original_path) in &paths_to_replace {
3032                    let answer = cx.update(|window, cx| {
3033                        window
3034                            .prompt(
3035                                PromptLevel::Info,
3036                                format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(),
3037                                None,
3038                                &["Replace", "Cancel"],
3039                                cx,
3040                            )
3041                    })?.await?;
3042
3043                    if answer == 1 {
3044                        if let Some(item_idx) = paths.iter().position(|p| p == original_path) {
3045                            paths.remove(item_idx);
3046                        }
3047                    }
3048                }
3049
3050                if paths.is_empty() {
3051                    return Ok(());
3052                }
3053
3054                let task = worktree.update( cx, |worktree, cx| {
3055                    worktree.copy_external_entries(target_directory.into(), paths, fs, cx)
3056                })?;
3057
3058                let opened_entries = task.await.with_context(|| "failed to copy external paths")?;
3059                this.update(cx, |this, cx| {
3060                    if open_file_after_drop && !opened_entries.is_empty() {
3061                        this.open_entry(opened_entries[0], true, false, cx);
3062                    }
3063                })
3064            }
3065            .log_err().await
3066        })
3067        .detach();
3068    }
3069
3070    fn drag_onto(
3071        &mut self,
3072        selections: &DraggedSelection,
3073        target_entry_id: ProjectEntryId,
3074        is_file: bool,
3075        window: &mut Window,
3076        cx: &mut Context<Self>,
3077    ) {
3078        let should_copy = window.modifiers().alt;
3079        if should_copy {
3080            let _ = maybe!({
3081                let project = self.project.read(cx);
3082                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
3083                let worktree_id = target_worktree.read(cx).id();
3084                let target_entry = target_worktree
3085                    .read(cx)
3086                    .entry_for_id(target_entry_id)?
3087                    .clone();
3088
3089                let mut copy_tasks = Vec::new();
3090                let mut disambiguation_range = None;
3091                for selection in selections.items() {
3092                    let (new_path, new_disambiguation_range) = self.create_paste_path(
3093                        selection,
3094                        (target_worktree.clone(), &target_entry),
3095                        cx,
3096                    )?;
3097
3098                    let task = self.project.update(cx, |project, cx| {
3099                        project.copy_entry(selection.entry_id, None, new_path, cx)
3100                    });
3101                    copy_tasks.push(task);
3102                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
3103                }
3104
3105                let item_count = copy_tasks.len();
3106
3107                cx.spawn_in(window, async move |project_panel, cx| {
3108                    let mut last_succeed = None;
3109                    for task in copy_tasks.into_iter() {
3110                        if let Some(Some(entry)) = task.await.log_err() {
3111                            last_succeed = Some(entry.id);
3112                        }
3113                    }
3114                    // update selection
3115                    if let Some(entry_id) = last_succeed {
3116                        project_panel
3117                            .update_in(cx, |project_panel, window, cx| {
3118                                project_panel.selection = Some(SelectedEntry {
3119                                    worktree_id,
3120                                    entry_id,
3121                                });
3122
3123                                // if only one entry was dragged and it was disambiguated, open the rename editor
3124                                if item_count == 1 && disambiguation_range.is_some() {
3125                                    project_panel.rename_impl(disambiguation_range, window, cx);
3126                                }
3127                            })
3128                            .ok();
3129                    }
3130                })
3131                .detach();
3132                Some(())
3133            });
3134        } else {
3135            for selection in selections.items() {
3136                self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
3137            }
3138        }
3139    }
3140
3141    fn index_for_entry(
3142        &self,
3143        entry_id: ProjectEntryId,
3144        worktree_id: WorktreeId,
3145    ) -> Option<(usize, usize, usize)> {
3146        let mut worktree_ix = 0;
3147        let mut total_ix = 0;
3148        for (current_worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3149            if worktree_id != *current_worktree_id {
3150                total_ix += visible_worktree_entries.len();
3151                worktree_ix += 1;
3152                continue;
3153            }
3154
3155            return visible_worktree_entries
3156                .iter()
3157                .enumerate()
3158                .find(|(_, entry)| entry.id == entry_id)
3159                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
3160        }
3161        None
3162    }
3163
3164    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
3165        let mut offset = 0;
3166        for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3167            if visible_worktree_entries.len() > offset + index {
3168                return visible_worktree_entries
3169                    .get(index)
3170                    .map(|entry| (*worktree_id, entry.to_ref()));
3171            }
3172            offset += visible_worktree_entries.len();
3173        }
3174        None
3175    }
3176
3177    fn iter_visible_entries(
3178        &self,
3179        range: Range<usize>,
3180        window: &mut Window,
3181        cx: &mut Context<ProjectPanel>,
3182        mut callback: impl FnMut(&Entry, &HashSet<Arc<Path>>, &mut Window, &mut Context<ProjectPanel>),
3183    ) {
3184        let mut ix = 0;
3185        for (_, visible_worktree_entries, entries_paths) in &self.visible_entries {
3186            if ix >= range.end {
3187                return;
3188            }
3189
3190            if ix + visible_worktree_entries.len() <= range.start {
3191                ix += visible_worktree_entries.len();
3192                continue;
3193            }
3194
3195            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3196            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3197            let entries = entries_paths.get_or_init(|| {
3198                visible_worktree_entries
3199                    .iter()
3200                    .map(|e| (e.path.clone()))
3201                    .collect()
3202            });
3203            for entry in visible_worktree_entries[entry_range].iter() {
3204                callback(&entry, entries, window, cx);
3205            }
3206            ix = end_ix;
3207        }
3208    }
3209
3210    fn for_each_visible_entry(
3211        &self,
3212        range: Range<usize>,
3213        window: &mut Window,
3214        cx: &mut Context<ProjectPanel>,
3215        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>),
3216    ) {
3217        let mut ix = 0;
3218        for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
3219            if ix >= range.end {
3220                return;
3221            }
3222
3223            if ix + visible_worktree_entries.len() <= range.start {
3224                ix += visible_worktree_entries.len();
3225                continue;
3226            }
3227
3228            let end_ix = range.end.min(ix + visible_worktree_entries.len());
3229            let (git_status_setting, show_file_icons, show_folder_icons) = {
3230                let settings = ProjectPanelSettings::get_global(cx);
3231                (
3232                    settings.git_status,
3233                    settings.file_icons,
3234                    settings.folder_icons,
3235                )
3236            };
3237            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
3238                let snapshot = worktree.read(cx).snapshot();
3239                let root_name = OsStr::new(snapshot.root_name());
3240                let expanded_entry_ids = self
3241                    .expanded_dir_ids
3242                    .get(&snapshot.id())
3243                    .map(Vec::as_slice)
3244                    .unwrap_or(&[]);
3245
3246                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3247                let entries = entries_paths.get_or_init(|| {
3248                    visible_worktree_entries
3249                        .iter()
3250                        .map(|e| (e.path.clone()))
3251                        .collect()
3252                });
3253                for entry in visible_worktree_entries[entry_range].iter() {
3254                    let status = git_status_setting
3255                        .then_some(entry.git_summary)
3256                        .unwrap_or_default();
3257                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
3258                    let icon = match entry.kind {
3259                        EntryKind::File => {
3260                            if show_file_icons {
3261                                FileIcons::get_icon(&entry.path, cx)
3262                            } else {
3263                                None
3264                            }
3265                        }
3266                        _ => {
3267                            if show_folder_icons {
3268                                FileIcons::get_folder_icon(is_expanded, cx)
3269                            } else {
3270                                FileIcons::get_chevron_icon(is_expanded, cx)
3271                            }
3272                        }
3273                    };
3274
3275                    let (depth, difference) =
3276                        ProjectPanel::calculate_depth_and_difference(&entry, entries);
3277
3278                    let filename = match difference {
3279                        diff if diff > 1 => entry
3280                            .path
3281                            .iter()
3282                            .skip(entry.path.components().count() - diff)
3283                            .collect::<PathBuf>()
3284                            .to_str()
3285                            .unwrap_or_default()
3286                            .to_string(),
3287                        _ => entry
3288                            .path
3289                            .file_name()
3290                            .map(|name| name.to_string_lossy().into_owned())
3291                            .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
3292                    };
3293                    let selection = SelectedEntry {
3294                        worktree_id: snapshot.id(),
3295                        entry_id: entry.id,
3296                    };
3297
3298                    let is_marked = self.marked_entries.contains(&selection);
3299
3300                    let diagnostic_severity = self
3301                        .diagnostics
3302                        .get(&(*worktree_id, entry.path.to_path_buf()))
3303                        .cloned();
3304
3305                    let filename_text_color =
3306                        entry_git_aware_label_color(status, entry.is_ignored, is_marked);
3307
3308                    let mut details = EntryDetails {
3309                        filename,
3310                        icon,
3311                        path: entry.path.clone(),
3312                        depth,
3313                        kind: entry.kind,
3314                        is_ignored: entry.is_ignored,
3315                        is_expanded,
3316                        is_selected: self.selection == Some(selection),
3317                        is_marked,
3318                        is_editing: false,
3319                        is_processing: false,
3320                        is_cut: self
3321                            .clipboard
3322                            .as_ref()
3323                            .map_or(false, |e| e.is_cut() && e.items().contains(&selection)),
3324                        filename_text_color,
3325                        diagnostic_severity,
3326                        git_status: status,
3327                        is_private: entry.is_private,
3328                        worktree_id: *worktree_id,
3329                        canonical_path: entry.canonical_path.clone(),
3330                    };
3331
3332                    if let Some(edit_state) = &self.edit_state {
3333                        let is_edited_entry = if edit_state.is_new_entry() {
3334                            entry.id == NEW_ENTRY_ID
3335                        } else {
3336                            entry.id == edit_state.entry_id
3337                                || self
3338                                    .ancestors
3339                                    .get(&entry.id)
3340                                    .is_some_and(|auto_folded_dirs| {
3341                                        auto_folded_dirs
3342                                            .ancestors
3343                                            .iter()
3344                                            .any(|entry_id| *entry_id == edit_state.entry_id)
3345                                    })
3346                        };
3347
3348                        if is_edited_entry {
3349                            if let Some(processing_filename) = &edit_state.processing_filename {
3350                                details.is_processing = true;
3351                                if let Some(ancestors) = edit_state
3352                                    .leaf_entry_id
3353                                    .and_then(|entry| self.ancestors.get(&entry))
3354                                {
3355                                    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;
3356                                    let all_components = ancestors.ancestors.len();
3357
3358                                    let prefix_components = all_components - position;
3359                                    let suffix_components = position.checked_sub(1);
3360                                    let mut previous_components =
3361                                        Path::new(&details.filename).components();
3362                                    let mut new_path = previous_components
3363                                        .by_ref()
3364                                        .take(prefix_components)
3365                                        .collect::<PathBuf>();
3366                                    if let Some(last_component) =
3367                                        Path::new(processing_filename).components().next_back()
3368                                    {
3369                                        new_path.push(last_component);
3370                                        previous_components.next();
3371                                    }
3372
3373                                    if let Some(_) = suffix_components {
3374                                        new_path.push(previous_components);
3375                                    }
3376                                    if let Some(str) = new_path.to_str() {
3377                                        details.filename.clear();
3378                                        details.filename.push_str(str);
3379                                    }
3380                                } else {
3381                                    details.filename.clear();
3382                                    details.filename.push_str(processing_filename);
3383                                }
3384                            } else {
3385                                if edit_state.is_new_entry() {
3386                                    details.filename.clear();
3387                                }
3388                                details.is_editing = true;
3389                            }
3390                        }
3391                    }
3392
3393                    callback(entry.id, details, window, cx);
3394                }
3395            }
3396            ix = end_ix;
3397        }
3398    }
3399
3400    fn find_entry_in_worktree(
3401        &self,
3402        worktree_id: WorktreeId,
3403        reverse_search: bool,
3404        only_visible_entries: bool,
3405        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3406        cx: &mut Context<Self>,
3407    ) -> Option<GitEntry> {
3408        if only_visible_entries {
3409            let entries = self
3410                .visible_entries
3411                .iter()
3412                .find_map(|(tree_id, entries, _)| {
3413                    if worktree_id == *tree_id {
3414                        Some(entries)
3415                    } else {
3416                        None
3417                    }
3418                })?
3419                .clone();
3420
3421            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
3422                .find(|ele| predicate(ele.to_ref(), worktree_id))
3423                .cloned();
3424        }
3425
3426        let repo_snapshots = self
3427            .project
3428            .read(cx)
3429            .git_store()
3430            .read(cx)
3431            .repo_snapshots(cx);
3432        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
3433        worktree.update(cx, |tree, _| {
3434            utils::ReversibleIterable::new(
3435                GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
3436                reverse_search,
3437            )
3438            .find_single_ended(|ele| predicate(*ele, worktree_id))
3439            .map(|ele| ele.to_owned())
3440        })
3441    }
3442
3443    fn find_entry(
3444        &self,
3445        start: Option<&SelectedEntry>,
3446        reverse_search: bool,
3447        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3448        cx: &mut Context<Self>,
3449    ) -> Option<SelectedEntry> {
3450        let mut worktree_ids: Vec<_> = self
3451            .visible_entries
3452            .iter()
3453            .map(|(worktree_id, _, _)| *worktree_id)
3454            .collect();
3455        let repo_snapshots = self
3456            .project
3457            .read(cx)
3458            .git_store()
3459            .read(cx)
3460            .repo_snapshots(cx);
3461
3462        let mut last_found: Option<SelectedEntry> = None;
3463
3464        if let Some(start) = start {
3465            let worktree = self
3466                .project
3467                .read(cx)
3468                .worktree_for_id(start.worktree_id, cx)?;
3469
3470            let search = worktree.update(cx, |tree, _| {
3471                let entry = tree.entry_for_id(start.entry_id)?;
3472                let root_entry = tree.root_entry()?;
3473                let tree_id = tree.id();
3474
3475                let mut first_iter = GitTraversal::new(
3476                    &repo_snapshots,
3477                    tree.traverse_from_path(true, true, true, entry.path.as_ref()),
3478                );
3479
3480                if reverse_search {
3481                    first_iter.next();
3482                }
3483
3484                let first = first_iter
3485                    .enumerate()
3486                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
3487                    .map(|(_, entry)| entry)
3488                    .find(|ele| predicate(*ele, tree_id))
3489                    .map(|ele| ele.to_owned());
3490
3491                let second_iter = GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize));
3492
3493                let second = if reverse_search {
3494                    second_iter
3495                        .take_until(|ele| ele.id == start.entry_id)
3496                        .filter(|ele| predicate(*ele, tree_id))
3497                        .last()
3498                        .map(|ele| ele.to_owned())
3499                } else {
3500                    second_iter
3501                        .take_while(|ele| ele.id != start.entry_id)
3502                        .filter(|ele| predicate(*ele, tree_id))
3503                        .last()
3504                        .map(|ele| ele.to_owned())
3505                };
3506
3507                if reverse_search {
3508                    Some((second, first))
3509                } else {
3510                    Some((first, second))
3511                }
3512            });
3513
3514            if let Some((first, second)) = search {
3515                let first = first.map(|entry| SelectedEntry {
3516                    worktree_id: start.worktree_id,
3517                    entry_id: entry.id,
3518                });
3519
3520                let second = second.map(|entry| SelectedEntry {
3521                    worktree_id: start.worktree_id,
3522                    entry_id: entry.id,
3523                });
3524
3525                if first.is_some() {
3526                    return first;
3527                }
3528                last_found = second;
3529
3530                let idx = worktree_ids
3531                    .iter()
3532                    .enumerate()
3533                    .find(|(_, ele)| **ele == start.worktree_id)
3534                    .map(|(idx, _)| idx);
3535
3536                if let Some(idx) = idx {
3537                    worktree_ids.rotate_left(idx + 1usize);
3538                    worktree_ids.pop();
3539                }
3540            }
3541        }
3542
3543        for tree_id in worktree_ids.into_iter() {
3544            if let Some(found) =
3545                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
3546            {
3547                return Some(SelectedEntry {
3548                    worktree_id: tree_id,
3549                    entry_id: found.id,
3550                });
3551            }
3552        }
3553
3554        last_found
3555    }
3556
3557    fn find_visible_entry(
3558        &self,
3559        start: Option<&SelectedEntry>,
3560        reverse_search: bool,
3561        predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3562        cx: &mut Context<Self>,
3563    ) -> Option<SelectedEntry> {
3564        let mut worktree_ids: Vec<_> = self
3565            .visible_entries
3566            .iter()
3567            .map(|(worktree_id, _, _)| *worktree_id)
3568            .collect();
3569
3570        let mut last_found: Option<SelectedEntry> = None;
3571
3572        if let Some(start) = start {
3573            let entries = self
3574                .visible_entries
3575                .iter()
3576                .find(|(worktree_id, _, _)| *worktree_id == start.worktree_id)
3577                .map(|(_, entries, _)| entries)?;
3578
3579            let mut start_idx = entries
3580                .iter()
3581                .enumerate()
3582                .find(|(_, ele)| ele.id == start.entry_id)
3583                .map(|(idx, _)| idx)?;
3584
3585            if reverse_search {
3586                start_idx = start_idx.saturating_add(1usize);
3587            }
3588
3589            let (left, right) = entries.split_at_checked(start_idx)?;
3590
3591            let (first_iter, second_iter) = if reverse_search {
3592                (
3593                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3594                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3595                )
3596            } else {
3597                (
3598                    utils::ReversibleIterable::new(right.iter(), reverse_search),
3599                    utils::ReversibleIterable::new(left.iter(), reverse_search),
3600                )
3601            };
3602
3603            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3604            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3605
3606            if first_search.is_some() {
3607                return first_search.map(|entry| SelectedEntry {
3608                    worktree_id: start.worktree_id,
3609                    entry_id: entry.id,
3610                });
3611            }
3612
3613            last_found = second_search.map(|entry| SelectedEntry {
3614                worktree_id: start.worktree_id,
3615                entry_id: entry.id,
3616            });
3617
3618            let idx = worktree_ids
3619                .iter()
3620                .enumerate()
3621                .find(|(_, ele)| **ele == start.worktree_id)
3622                .map(|(idx, _)| idx);
3623
3624            if let Some(idx) = idx {
3625                worktree_ids.rotate_left(idx + 1usize);
3626                worktree_ids.pop();
3627            }
3628        }
3629
3630        for tree_id in worktree_ids.into_iter() {
3631            if let Some(found) =
3632                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
3633            {
3634                return Some(SelectedEntry {
3635                    worktree_id: tree_id,
3636                    entry_id: found.id,
3637                });
3638            }
3639        }
3640
3641        last_found
3642    }
3643
3644    fn calculate_depth_and_difference(
3645        entry: &Entry,
3646        visible_worktree_entries: &HashSet<Arc<Path>>,
3647    ) -> (usize, usize) {
3648        let (depth, difference) = entry
3649            .path
3650            .ancestors()
3651            .skip(1) // Skip the entry itself
3652            .find_map(|ancestor| {
3653                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
3654                    let entry_path_components_count = entry.path.components().count();
3655                    let parent_path_components_count = parent_entry.components().count();
3656                    let difference = entry_path_components_count - parent_path_components_count;
3657                    let depth = parent_entry
3658                        .ancestors()
3659                        .skip(1)
3660                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
3661                        .count();
3662                    Some((depth + 1, difference))
3663                } else {
3664                    None
3665                }
3666            })
3667            .unwrap_or((0, 0));
3668
3669        (depth, difference)
3670    }
3671
3672    fn render_entry(
3673        &self,
3674        entry_id: ProjectEntryId,
3675        details: EntryDetails,
3676        window: &mut Window,
3677        cx: &mut Context<Self>,
3678    ) -> Stateful<Div> {
3679        const GROUP_NAME: &str = "project_entry";
3680
3681        let kind = details.kind;
3682        let settings = ProjectPanelSettings::get_global(cx);
3683        let show_editor = details.is_editing && !details.is_processing;
3684
3685        let selection = SelectedEntry {
3686            worktree_id: details.worktree_id,
3687            entry_id,
3688        };
3689
3690        let is_marked = self.marked_entries.contains(&selection);
3691        let is_active = self
3692            .selection
3693            .map_or(false, |selection| selection.entry_id == entry_id);
3694
3695        let file_name = details.filename.clone();
3696
3697        let mut icon = details.icon.clone();
3698        if settings.file_icons && show_editor && details.kind.is_file() {
3699            let filename = self.filename_editor.read(cx).text(cx);
3700            if filename.len() > 2 {
3701                icon = FileIcons::get_icon(Path::new(&filename), cx);
3702            }
3703        }
3704
3705        let filename_text_color = details.filename_text_color;
3706        let diagnostic_severity = details.diagnostic_severity;
3707        let item_colors = get_item_color(cx);
3708
3709        let canonical_path = details
3710            .canonical_path
3711            .as_ref()
3712            .map(|f| f.to_string_lossy().to_string());
3713        let path = details.path.clone();
3714
3715        let depth = details.depth;
3716        let worktree_id = details.worktree_id;
3717        let selections = Arc::new(self.marked_entries.clone());
3718
3719        let dragged_selection = DraggedSelection {
3720            active_selection: selection,
3721            marked_selections: selections,
3722        };
3723
3724        let bg_color = if is_marked {
3725            item_colors.marked
3726        } else {
3727            item_colors.default
3728        };
3729
3730        let bg_hover_color = if is_marked {
3731            item_colors.marked
3732        } else {
3733            item_colors.hover
3734        };
3735
3736        let validation_color_and_message = if show_editor {
3737            match self
3738                .edit_state
3739                .as_ref()
3740                .map_or(ValidationState::None, |e| e.validation_state.clone())
3741            {
3742                ValidationState::Error(msg) => Some((Color::Error.color(cx), msg.clone())),
3743                ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg.clone())),
3744                ValidationState::None => None,
3745            }
3746        } else {
3747            None
3748        };
3749
3750        let border_color =
3751            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3752                match validation_color_and_message {
3753                    Some((color, _)) => color,
3754                    None => item_colors.focused,
3755                }
3756            } else {
3757                bg_color
3758            };
3759
3760        let border_hover_color =
3761            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3762                match validation_color_and_message {
3763                    Some((color, _)) => color,
3764                    None => item_colors.focused,
3765                }
3766            } else {
3767                bg_hover_color
3768            };
3769
3770        let folded_directory_drag_target = self.folded_directory_drag_target;
3771
3772        div()
3773            .id(entry_id.to_proto() as usize)
3774            .group(GROUP_NAME)
3775            .cursor_pointer()
3776            .rounded_none()
3777            .bg(bg_color)
3778            .border_1()
3779            .border_r_2()
3780            .border_color(border_color)
3781            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
3782            .on_drag_move::<ExternalPaths>(cx.listener(
3783                move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
3784                    if event.bounds.contains(&event.event.position) {
3785                        if this.last_external_paths_drag_over_entry == Some(entry_id) {
3786                            return;
3787                        }
3788                        this.last_external_paths_drag_over_entry = Some(entry_id);
3789                        this.marked_entries.clear();
3790
3791                        let Some((worktree, path, entry)) = maybe!({
3792                            let worktree = this
3793                                .project
3794                                .read(cx)
3795                                .worktree_for_id(selection.worktree_id, cx)?;
3796                            let worktree = worktree.read(cx);
3797                            let entry = worktree.entry_for_path(&path)?;
3798                            let path = if entry.is_dir() {
3799                                path.as_ref()
3800                            } else {
3801                                path.parent()?
3802                            };
3803                            Some((worktree, path, entry))
3804                        }) else {
3805                            return;
3806                        };
3807
3808                        this.marked_entries.insert(SelectedEntry {
3809                            entry_id: entry.id,
3810                            worktree_id: worktree.id(),
3811                        });
3812
3813                        for entry in worktree.child_entries(path) {
3814                            this.marked_entries.insert(SelectedEntry {
3815                                entry_id: entry.id,
3816                                worktree_id: worktree.id(),
3817                            });
3818                        }
3819
3820                        cx.notify();
3821                    }
3822                },
3823            ))
3824            .on_drop(cx.listener(
3825                move |this, external_paths: &ExternalPaths, window, cx| {
3826                    this.hover_scroll_task.take();
3827                    this.last_external_paths_drag_over_entry = None;
3828                    this.marked_entries.clear();
3829                    this.drop_external_files(external_paths.paths(), entry_id, window, cx);
3830                    cx.stop_propagation();
3831                },
3832            ))
3833            .on_drag_move::<DraggedSelection>(cx.listener(
3834                move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
3835                    if event.bounds.contains(&event.event.position) {
3836                        if this.last_selection_drag_over_entry == Some(entry_id) {
3837                            return;
3838                        }
3839                        this.last_selection_drag_over_entry = Some(entry_id);
3840                        this.hover_expand_task.take();
3841
3842                        if !kind.is_dir()
3843                            || this
3844                                .expanded_dir_ids
3845                                .get(&details.worktree_id)
3846                                .map_or(false, |ids| ids.binary_search(&entry_id).is_ok())
3847                        {
3848                            return;
3849                        }
3850
3851                        let bounds = event.bounds;
3852                        this.hover_expand_task =
3853                            Some(cx.spawn_in(window, async move |this, cx| {
3854                                cx.background_executor()
3855                                    .timer(Duration::from_millis(500))
3856                                    .await;
3857                                this.update_in(cx, |this, window, cx| {
3858                                    this.hover_expand_task.take();
3859                                    if this.last_selection_drag_over_entry == Some(entry_id)
3860                                        && bounds.contains(&window.mouse_position())
3861                                    {
3862                                        this.expand_entry(worktree_id, entry_id, cx);
3863                                        this.update_visible_entries(
3864                                            Some((worktree_id, entry_id)),
3865                                            cx,
3866                                        );
3867                                        cx.notify();
3868                                    }
3869                                })
3870                                .ok();
3871                            }));
3872                    }
3873                },
3874            ))
3875            .on_drag(
3876                dragged_selection,
3877                move |selection, click_offset, _window, cx| {
3878                    cx.new(|_| DraggedProjectEntryView {
3879                        details: details.clone(),
3880                        click_offset,
3881                        selection: selection.active_selection,
3882                        selections: selection.marked_selections.clone(),
3883                    })
3884                },
3885            )
3886            .drag_over::<DraggedSelection>(move |style, _, _, _| {
3887                if  folded_directory_drag_target.is_some() {
3888                    return style;
3889                }
3890                style.bg(item_colors.drag_over)
3891            })
3892            .on_drop(
3893                cx.listener(move |this, selections: &DraggedSelection, window, cx| {
3894                    this.hover_scroll_task.take();
3895                    this.hover_expand_task.take();
3896                    if  folded_directory_drag_target.is_some() {
3897                        return;
3898                    }
3899                    this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
3900                }),
3901            )
3902            .on_mouse_down(
3903                MouseButton::Left,
3904                cx.listener(move |this, _, _, cx| {
3905                    this.mouse_down = true;
3906                    cx.propagate();
3907                }),
3908            )
3909            .on_click(
3910                cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
3911                    if event.down.button == MouseButton::Right
3912                        || event.down.first_mouse
3913                        || show_editor
3914                    {
3915                        return;
3916                    }
3917                    if event.down.button == MouseButton::Left {
3918                        this.mouse_down = false;
3919                    }
3920                    cx.stop_propagation();
3921
3922                    if let Some(selection) = this.selection.filter(|_| event.modifiers().shift) {
3923                        let current_selection = this.index_for_selection(selection);
3924                        let clicked_entry = SelectedEntry {
3925                            entry_id,
3926                            worktree_id,
3927                        };
3928                        let target_selection = this.index_for_selection(clicked_entry);
3929                        if let Some(((_, _, source_index), (_, _, target_index))) =
3930                            current_selection.zip(target_selection)
3931                        {
3932                            let range_start = source_index.min(target_index);
3933                            let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
3934                            let mut new_selections = BTreeSet::new();
3935                            this.for_each_visible_entry(
3936                                range_start..range_end,
3937                                window,
3938                                cx,
3939                                |entry_id, details, _, _| {
3940                                    new_selections.insert(SelectedEntry {
3941                                        entry_id,
3942                                        worktree_id: details.worktree_id,
3943                                    });
3944                                },
3945                            );
3946
3947                            this.marked_entries = this
3948                                .marked_entries
3949                                .union(&new_selections)
3950                                .cloned()
3951                                .collect();
3952
3953                            this.selection = Some(clicked_entry);
3954                            this.marked_entries.insert(clicked_entry);
3955                        }
3956                    } else if event.modifiers().secondary() {
3957                        if event.down.click_count > 1 {
3958                            this.split_entry(entry_id, cx);
3959                        } else {
3960                            this.selection = Some(selection);
3961                            if !this.marked_entries.insert(selection) {
3962                                this.marked_entries.remove(&selection);
3963                            }
3964                        }
3965                    } else if kind.is_dir() {
3966                        this.marked_entries.clear();
3967                        if event.modifiers().alt {
3968                            this.toggle_expand_all(entry_id, window, cx);
3969                        } else {
3970                            this.toggle_expanded(entry_id, window, cx);
3971                        }
3972                    } else {
3973                        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
3974                        let click_count = event.up.click_count;
3975                        let focus_opened_item = !preview_tabs_enabled || click_count > 1;
3976                        let allow_preview = preview_tabs_enabled && click_count == 1;
3977                        this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
3978                    }
3979                }),
3980            )
3981            .child(
3982                ListItem::new(entry_id.to_proto() as usize)
3983                    .indent_level(depth)
3984                    .indent_step_size(px(settings.indent_size))
3985                    .spacing(match settings.entry_spacing {
3986                        project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense,
3987                        project_panel_settings::EntrySpacing::Standard => {
3988                            ListItemSpacing::ExtraDense
3989                        }
3990                    })
3991                    .selectable(false)
3992                    .when_some(canonical_path, |this, path| {
3993                        this.end_slot::<AnyElement>(
3994                            div()
3995                                .id("symlink_icon")
3996                                .pr_3()
3997                                .tooltip(move |window, cx| {
3998                                    Tooltip::with_meta(
3999                                        path.to_string(),
4000                                        None,
4001                                        "Symbolic Link",
4002                                        window,
4003                                        cx,
4004                                    )
4005                                })
4006                                .child(
4007                                    Icon::new(IconName::ArrowUpRight)
4008                                        .size(IconSize::Indicator)
4009                                        .color(filename_text_color),
4010                                )
4011                                .into_any_element(),
4012                        )
4013                    })
4014                    .child(if let Some(icon) = &icon {
4015                        if let Some((_, decoration_color)) =
4016                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
4017                        {
4018                            let is_warning = diagnostic_severity
4019                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
4020                                .unwrap_or(false);
4021                            div().child(
4022                                DecoratedIcon::new(
4023                                    Icon::from_path(icon.clone()).color(Color::Muted),
4024                                    Some(
4025                                        IconDecoration::new(
4026                                            if kind.is_file() {
4027                                                if is_warning {
4028                                                    IconDecorationKind::Triangle
4029                                                } else {
4030                                                    IconDecorationKind::X
4031                                                }
4032                                            } else {
4033                                                IconDecorationKind::Dot
4034                                            },
4035                                            bg_color,
4036                                            cx,
4037                                        )
4038                                        .group_name(Some(GROUP_NAME.into()))
4039                                        .knockout_hover_color(bg_hover_color)
4040                                        .color(decoration_color.color(cx))
4041                                        .position(Point {
4042                                            x: px(-2.),
4043                                            y: px(-2.),
4044                                        }),
4045                                    ),
4046                                )
4047                                .into_any_element(),
4048                            )
4049                        } else {
4050                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
4051                        }
4052                    } else {
4053                        if let Some((icon_name, color)) =
4054                            entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
4055                        {
4056                            h_flex()
4057                                .size(IconSize::default().rems())
4058                                .child(Icon::new(icon_name).color(color).size(IconSize::Small))
4059                        } else {
4060                            h_flex()
4061                                .size(IconSize::default().rems())
4062                                .invisible()
4063                                .flex_none()
4064                        }
4065                    })
4066                    .child(
4067                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
4068                            h_flex().h_6().w_full().child(editor.clone())
4069                        } else {
4070                            h_flex().h_6().map(|mut this| {
4071                                if let Some(folded_ancestors) = self.ancestors.get(&entry_id) {
4072                                    let components = Path::new(&file_name)
4073                                        .components()
4074                                        .map(|comp| {
4075                                            let comp_str =
4076                                                comp.as_os_str().to_string_lossy().into_owned();
4077                                            comp_str
4078                                        })
4079                                        .collect::<Vec<_>>();
4080
4081                                    let components_len = components.len();
4082                                    let active_index = components_len
4083                                        - 1
4084                                        - folded_ancestors.current_ancestor_depth;
4085                                        const DELIMITER: SharedString =
4086                                        SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
4087                                    for (index, component) in components.into_iter().enumerate() {
4088                                        if index != 0 {
4089                                                let delimiter_target_index = index - 1;
4090                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
4091                                                this = this.child(
4092                                                    div()
4093                                                    .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
4094                                                        this.hover_scroll_task.take();
4095                                                        this.folded_directory_drag_target = None;
4096                                                        if let Some(target_entry_id) = target_entry_id {
4097                                                            this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4098                                                        }
4099                                                    }))
4100                                                    .on_drag_move(cx.listener(
4101                                                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4102                                                            if event.bounds.contains(&event.event.position) {
4103                                                                this.folded_directory_drag_target = Some(
4104                                                                    FoldedDirectoryDragTarget {
4105                                                                        entry_id,
4106                                                                        index: delimiter_target_index,
4107                                                                        is_delimiter_target: true,
4108                                                                    }
4109                                                                );
4110                                                            } else {
4111                                                                let is_current_target = this.folded_directory_drag_target
4112                                                                    .map_or(false, |target|
4113                                                                        target.entry_id == entry_id &&
4114                                                                        target.index == delimiter_target_index &&
4115                                                                        target.is_delimiter_target
4116                                                                    );
4117                                                                if is_current_target {
4118                                                                    this.folded_directory_drag_target = None;
4119                                                                }
4120                                                            }
4121
4122                                                        },
4123                                                    ))
4124                                                    .child(
4125                                                        Label::new(DELIMITER.clone())
4126                                                            .single_line()
4127                                                            .color(filename_text_color)
4128                                                    )
4129                                                );
4130                                        }
4131                                        let id = SharedString::from(format!(
4132                                            "project_panel_path_component_{}_{index}",
4133                                            entry_id.to_usize()
4134                                        ));
4135                                        let label = div()
4136                                            .id(id)
4137                                            .on_click(cx.listener(move |this, _, _, cx| {
4138                                                if index != active_index {
4139                                                    if let Some(folds) =
4140                                                        this.ancestors.get_mut(&entry_id)
4141                                                    {
4142                                                        folds.current_ancestor_depth =
4143                                                            components_len - 1 - index;
4144                                                        cx.notify();
4145                                                    }
4146                                                }
4147                                            }))
4148                                            .when(index != components_len - 1, |div|{
4149                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
4150                                                div
4151                                                .on_drag_move(cx.listener(
4152                                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4153                                                    if event.bounds.contains(&event.event.position) {
4154                                                            this.folded_directory_drag_target = Some(
4155                                                                FoldedDirectoryDragTarget {
4156                                                                    entry_id,
4157                                                                    index,
4158                                                                    is_delimiter_target: false,
4159                                                                }
4160                                                            );
4161                                                        } else {
4162                                                            let is_current_target = this.folded_directory_drag_target
4163                                                                .as_ref()
4164                                                                .map_or(false, |target|
4165                                                                    target.entry_id == entry_id &&
4166                                                                    target.index == index &&
4167                                                                    !target.is_delimiter_target
4168                                                                );
4169                                                            if is_current_target {
4170                                                                this.folded_directory_drag_target = None;
4171                                                            }
4172                                                        }
4173                                                    },
4174                                                ))
4175                                                .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| {
4176                                                    this.hover_scroll_task.take();
4177                                                    this.folded_directory_drag_target = None;
4178                                                    if let Some(target_entry_id) = target_entry_id {
4179                                                        this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4180                                                    }
4181                                                }))
4182                                                .when(folded_directory_drag_target.map_or(false, |target|
4183                                                    target.entry_id == entry_id &&
4184                                                    target.index == index
4185                                                ), |this| {
4186                                                    this.bg(item_colors.drag_over)
4187                                                })
4188                                            })
4189                                            .child(
4190                                                Label::new(component)
4191                                                    .single_line()
4192                                                    .color(filename_text_color)
4193                                                    .when(
4194                                                        index == active_index
4195                                                            && (is_active || is_marked),
4196                                                        |this| this.underline(),
4197                                                    ),
4198                                            );
4199
4200                                        this = this.child(label);
4201                                    }
4202
4203                                    this
4204                                } else {
4205                                    this.child(
4206                                        Label::new(file_name)
4207                                            .single_line()
4208                                            .color(filename_text_color),
4209                                    )
4210                                }
4211                            })
4212                        }
4213                        .ml_1(),
4214                    )
4215                    .on_secondary_mouse_down(cx.listener(
4216                        move |this, event: &MouseDownEvent, window, cx| {
4217                            // Stop propagation to prevent the catch-all context menu for the project
4218                            // panel from being deployed.
4219                            cx.stop_propagation();
4220                            // Some context menu actions apply to all marked entries. If the user
4221                            // right-clicks on an entry that is not marked, they may not realize the
4222                            // action applies to multiple entries. To avoid inadvertent changes, all
4223                            // entries are unmarked.
4224                            if !this.marked_entries.contains(&selection) {
4225                                this.marked_entries.clear();
4226                            }
4227                            this.deploy_context_menu(event.position, entry_id, window, cx);
4228                        },
4229                    ))
4230                    .overflow_x(),
4231            )
4232            .when_some(
4233                validation_color_and_message,
4234                |this, (color, message)| {
4235                    this
4236                    .relative()
4237                    .child(
4238                        deferred(
4239                            div()
4240                            .occlude()
4241                            .absolute()
4242                            .top_full()
4243                            .left(px(-1.)) // Used px over rem so that it doesn't change with font size
4244                            .right(px(-0.5))
4245                            .py_1()
4246                            .px_2()
4247                            .border_1()
4248                            .border_color(color)
4249                            .bg(cx.theme().colors().background)
4250                            .child(
4251                                Label::new(message)
4252                                .color(Color::from(color))
4253                                .size(LabelSize::Small)
4254                            )
4255                        )
4256                    )
4257                }
4258            )
4259    }
4260
4261    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4262        if !Self::should_show_scrollbar(cx)
4263            || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4264        {
4265            return None;
4266        }
4267        Some(
4268            div()
4269                .occlude()
4270                .id("project-panel-vertical-scroll")
4271                .on_mouse_move(cx.listener(|_, _, _, cx| {
4272                    cx.notify();
4273                    cx.stop_propagation()
4274                }))
4275                .on_hover(|_, _, cx| {
4276                    cx.stop_propagation();
4277                })
4278                .on_any_mouse_down(|_, _, cx| {
4279                    cx.stop_propagation();
4280                })
4281                .on_mouse_up(
4282                    MouseButton::Left,
4283                    cx.listener(|this, _, window, cx| {
4284                        if !this.vertical_scrollbar_state.is_dragging()
4285                            && !this.focus_handle.contains_focused(window, cx)
4286                        {
4287                            this.hide_scrollbar(window, cx);
4288                            cx.notify();
4289                        }
4290
4291                        cx.stop_propagation();
4292                    }),
4293                )
4294                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4295                    cx.notify();
4296                }))
4297                .h_full()
4298                .absolute()
4299                .right_1()
4300                .top_1()
4301                .bottom_1()
4302                .w(px(12.))
4303                .cursor_default()
4304                .children(Scrollbar::vertical(
4305                    // percentage as f32..end_offset as f32,
4306                    self.vertical_scrollbar_state.clone(),
4307                )),
4308        )
4309    }
4310
4311    fn render_horizontal_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4312        if !Self::should_show_scrollbar(cx)
4313            || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4314        {
4315            return None;
4316        }
4317
4318        let scroll_handle = self.scroll_handle.0.borrow();
4319        let longest_item_width = scroll_handle
4320            .last_item_size
4321            .filter(|size| size.contents.width > size.item.width)?
4322            .contents
4323            .width
4324            .0 as f64;
4325        if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
4326            return None;
4327        }
4328
4329        Some(
4330            div()
4331                .occlude()
4332                .id("project-panel-horizontal-scroll")
4333                .on_mouse_move(cx.listener(|_, _, _, cx| {
4334                    cx.notify();
4335                    cx.stop_propagation()
4336                }))
4337                .on_hover(|_, _, cx| {
4338                    cx.stop_propagation();
4339                })
4340                .on_any_mouse_down(|_, _, cx| {
4341                    cx.stop_propagation();
4342                })
4343                .on_mouse_up(
4344                    MouseButton::Left,
4345                    cx.listener(|this, _, window, cx| {
4346                        if !this.horizontal_scrollbar_state.is_dragging()
4347                            && !this.focus_handle.contains_focused(window, cx)
4348                        {
4349                            this.hide_scrollbar(window, cx);
4350                            cx.notify();
4351                        }
4352
4353                        cx.stop_propagation();
4354                    }),
4355                )
4356                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4357                    cx.notify();
4358                }))
4359                .w_full()
4360                .absolute()
4361                .right_1()
4362                .left_1()
4363                .bottom_1()
4364                .h(px(12.))
4365                .cursor_default()
4366                .when(self.width.is_some(), |this| {
4367                    this.children(Scrollbar::horizontal(
4368                        self.horizontal_scrollbar_state.clone(),
4369                    ))
4370                }),
4371        )
4372    }
4373
4374    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
4375        let mut dispatch_context = KeyContext::new_with_defaults();
4376        dispatch_context.add("ProjectPanel");
4377        dispatch_context.add("menu");
4378
4379        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
4380            "editing"
4381        } else {
4382            "not_editing"
4383        };
4384
4385        dispatch_context.add(identifier);
4386        dispatch_context
4387    }
4388
4389    fn should_show_scrollbar(cx: &App) -> bool {
4390        let show = ProjectPanelSettings::get_global(cx)
4391            .scrollbar
4392            .show
4393            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4394        match show {
4395            ShowScrollbar::Auto => true,
4396            ShowScrollbar::System => true,
4397            ShowScrollbar::Always => true,
4398            ShowScrollbar::Never => false,
4399        }
4400    }
4401
4402    fn should_autohide_scrollbar(cx: &App) -> bool {
4403        let show = ProjectPanelSettings::get_global(cx)
4404            .scrollbar
4405            .show
4406            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4407        match show {
4408            ShowScrollbar::Auto => true,
4409            ShowScrollbar::System => cx
4410                .try_global::<ScrollbarAutoHide>()
4411                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4412            ShowScrollbar::Always => false,
4413            ShowScrollbar::Never => true,
4414        }
4415    }
4416
4417    fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4418        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4419        if !Self::should_autohide_scrollbar(cx) {
4420            return;
4421        }
4422        self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4423            cx.background_executor()
4424                .timer(SCROLLBAR_SHOW_INTERVAL)
4425                .await;
4426            panel
4427                .update(cx, |panel, cx| {
4428                    panel.show_scrollbar = false;
4429                    cx.notify();
4430                })
4431                .log_err();
4432        }))
4433    }
4434
4435    fn reveal_entry(
4436        &mut self,
4437        project: Entity<Project>,
4438        entry_id: ProjectEntryId,
4439        skip_ignored: bool,
4440        cx: &mut Context<Self>,
4441    ) -> Result<()> {
4442        if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
4443            let worktree = worktree.read(cx);
4444            if skip_ignored
4445                && worktree
4446                    .entry_for_id(entry_id)
4447                    .map_or(true, |entry| entry.is_ignored && !entry.is_always_included)
4448            {
4449                return Err(anyhow!(
4450                    "can't reveal an ignored entry in the project panel"
4451                ));
4452            }
4453
4454            let worktree_id = worktree.id();
4455            self.expand_entry(worktree_id, entry_id, cx);
4456            self.update_visible_entries(Some((worktree_id, entry_id)), cx);
4457            self.marked_entries.clear();
4458            self.marked_entries.insert(SelectedEntry {
4459                worktree_id,
4460                entry_id,
4461            });
4462            self.autoscroll(cx);
4463            cx.notify();
4464            Ok(())
4465        } else {
4466            Err(anyhow!(
4467                "can't reveal a non-existent entry in the project panel"
4468            ))
4469        }
4470    }
4471
4472    fn find_active_indent_guide(
4473        &self,
4474        indent_guides: &[IndentGuideLayout],
4475        cx: &App,
4476    ) -> Option<usize> {
4477        let (worktree, entry) = self.selected_entry(cx)?;
4478
4479        // Find the parent entry of the indent guide, this will either be the
4480        // expanded folder we have selected, or the parent of the currently
4481        // selected file/collapsed directory
4482        let mut entry = entry;
4483        loop {
4484            let is_expanded_dir = entry.is_dir()
4485                && self
4486                    .expanded_dir_ids
4487                    .get(&worktree.id())
4488                    .map(|ids| ids.binary_search(&entry.id).is_ok())
4489                    .unwrap_or(false);
4490            if is_expanded_dir {
4491                break;
4492            }
4493            entry = worktree.entry_for_path(&entry.path.parent()?)?;
4494        }
4495
4496        let (active_indent_range, depth) = {
4497            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
4498            let child_paths = &self.visible_entries[worktree_ix].1;
4499            let mut child_count = 0;
4500            let depth = entry.path.ancestors().count();
4501            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
4502                if entry.path.ancestors().count() <= depth {
4503                    break;
4504                }
4505                child_count += 1;
4506            }
4507
4508            let start = ix + 1;
4509            let end = start + child_count;
4510
4511            let (_, entries, paths) = &self.visible_entries[worktree_ix];
4512            let visible_worktree_entries =
4513                paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect());
4514
4515            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
4516            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
4517            (start..end, depth)
4518        };
4519
4520        let candidates = indent_guides
4521            .iter()
4522            .enumerate()
4523            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
4524
4525        for (i, indent) in candidates {
4526            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
4527            if active_indent_range.start <= indent.offset.y + indent.length
4528                && indent.offset.y <= active_indent_range.end
4529            {
4530                return Some(i);
4531            }
4532        }
4533        None
4534    }
4535}
4536
4537fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
4538    const ICON_SIZE_FACTOR: usize = 2;
4539    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
4540    if is_symlink {
4541        item_width += ICON_SIZE_FACTOR;
4542    }
4543    item_width
4544}
4545
4546impl Render for ProjectPanel {
4547    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4548        let has_worktree = !self.visible_entries.is_empty();
4549        let project = self.project.read(cx);
4550        let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
4551        let show_indent_guides =
4552            ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
4553        let is_local = project.is_local();
4554
4555        if has_worktree {
4556            let item_count = self
4557                .visible_entries
4558                .iter()
4559                .map(|(_, worktree_entries, _)| worktree_entries.len())
4560                .sum();
4561
4562            fn handle_drag_move_scroll<T: 'static>(
4563                this: &mut ProjectPanel,
4564                e: &DragMoveEvent<T>,
4565                window: &mut Window,
4566                cx: &mut Context<ProjectPanel>,
4567            ) {
4568                if !e.bounds.contains(&e.event.position) {
4569                    return;
4570                }
4571                this.hover_scroll_task.take();
4572                let panel_height = e.bounds.size.height;
4573                if panel_height <= px(0.) {
4574                    return;
4575                }
4576
4577                let event_offset = e.event.position.y - e.bounds.origin.y;
4578                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
4579                let hovered_region_offset = event_offset / panel_height;
4580
4581                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
4582                // These pixels offsets were picked arbitrarily.
4583                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
4584                    8.
4585                } else if hovered_region_offset <= 0.15 {
4586                    5.
4587                } else if hovered_region_offset >= 0.95 {
4588                    -8.
4589                } else if hovered_region_offset >= 0.85 {
4590                    -5.
4591                } else {
4592                    return;
4593                };
4594                let adjustment = point(px(0.), px(vertical_scroll_offset));
4595                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
4596                    loop {
4597                        let should_stop_scrolling = this
4598                            .update(cx, |this, cx| {
4599                                this.hover_scroll_task.as_ref()?;
4600                                let handle = this.scroll_handle.0.borrow_mut();
4601                                let offset = handle.base_handle.offset();
4602
4603                                handle.base_handle.set_offset(offset + adjustment);
4604                                cx.notify();
4605                                Some(())
4606                            })
4607                            .ok()
4608                            .flatten()
4609                            .is_some();
4610                        if should_stop_scrolling {
4611                            return;
4612                        }
4613                        cx.background_executor()
4614                            .timer(Duration::from_millis(16))
4615                            .await;
4616                    }
4617                }));
4618            }
4619            h_flex()
4620                .id("project-panel")
4621                .group("project-panel")
4622                .on_drag_move(cx.listener(handle_drag_move_scroll::<ExternalPaths>))
4623                .on_drag_move(cx.listener(handle_drag_move_scroll::<DraggedSelection>))
4624                .size_full()
4625                .relative()
4626                .on_hover(cx.listener(|this, hovered, window, cx| {
4627                    if *hovered {
4628                        this.show_scrollbar = true;
4629                        this.hide_scrollbar_task.take();
4630                        cx.notify();
4631                    } else if !this.focus_handle.contains_focused(window, cx) {
4632                        this.hide_scrollbar(window, cx);
4633                    }
4634                }))
4635                .on_click(cx.listener(|this, _event, _, cx| {
4636                    cx.stop_propagation();
4637                    this.selection = None;
4638                    this.marked_entries.clear();
4639                }))
4640                .key_context(self.dispatch_context(window, cx))
4641                .on_action(cx.listener(Self::select_next))
4642                .on_action(cx.listener(Self::select_previous))
4643                .on_action(cx.listener(Self::select_first))
4644                .on_action(cx.listener(Self::select_last))
4645                .on_action(cx.listener(Self::select_parent))
4646                .on_action(cx.listener(Self::select_next_git_entry))
4647                .on_action(cx.listener(Self::select_prev_git_entry))
4648                .on_action(cx.listener(Self::select_next_diagnostic))
4649                .on_action(cx.listener(Self::select_prev_diagnostic))
4650                .on_action(cx.listener(Self::select_next_directory))
4651                .on_action(cx.listener(Self::select_prev_directory))
4652                .on_action(cx.listener(Self::expand_selected_entry))
4653                .on_action(cx.listener(Self::collapse_selected_entry))
4654                .on_action(cx.listener(Self::collapse_all_entries))
4655                .on_action(cx.listener(Self::open))
4656                .on_action(cx.listener(Self::open_permanent))
4657                .on_action(cx.listener(Self::confirm))
4658                .on_action(cx.listener(Self::cancel))
4659                .on_action(cx.listener(Self::copy_path))
4660                .on_action(cx.listener(Self::copy_relative_path))
4661                .on_action(cx.listener(Self::new_search_in_directory))
4662                .on_action(cx.listener(Self::unfold_directory))
4663                .on_action(cx.listener(Self::fold_directory))
4664                .on_action(cx.listener(Self::remove_from_project))
4665                .when(!project.is_read_only(cx), |el| {
4666                    el.on_action(cx.listener(Self::new_file))
4667                        .on_action(cx.listener(Self::new_directory))
4668                        .on_action(cx.listener(Self::rename))
4669                        .on_action(cx.listener(Self::delete))
4670                        .on_action(cx.listener(Self::trash))
4671                        .on_action(cx.listener(Self::cut))
4672                        .on_action(cx.listener(Self::copy))
4673                        .on_action(cx.listener(Self::paste))
4674                        .on_action(cx.listener(Self::duplicate))
4675                        .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| {
4676                            if event.up.click_count > 1 {
4677                                if let Some(entry_id) = this.last_worktree_root_id {
4678                                    let project = this.project.read(cx);
4679
4680                                    let worktree_id = if let Some(worktree) =
4681                                        project.worktree_for_entry(entry_id, cx)
4682                                    {
4683                                        worktree.read(cx).id()
4684                                    } else {
4685                                        return;
4686                                    };
4687
4688                                    this.selection = Some(SelectedEntry {
4689                                        worktree_id,
4690                                        entry_id,
4691                                    });
4692
4693                                    this.new_file(&NewFile, window, cx);
4694                                }
4695                            }
4696                        }))
4697                })
4698                .when(project.is_local(), |el| {
4699                    el.on_action(cx.listener(Self::reveal_in_finder))
4700                        .on_action(cx.listener(Self::open_system))
4701                        .on_action(cx.listener(Self::open_in_terminal))
4702                })
4703                .when(project.is_via_ssh(), |el| {
4704                    el.on_action(cx.listener(Self::open_in_terminal))
4705                })
4706                .on_mouse_down(
4707                    MouseButton::Right,
4708                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
4709                        // When deploying the context menu anywhere below the last project entry,
4710                        // act as if the user clicked the root of the last worktree.
4711                        if let Some(entry_id) = this.last_worktree_root_id {
4712                            this.deploy_context_menu(event.position, entry_id, window, cx);
4713                        }
4714                    }),
4715                )
4716                .track_focus(&self.focus_handle(cx))
4717                .child(
4718                    uniform_list(cx.entity().clone(), "entries", item_count, {
4719                        |this, range, window, cx| {
4720                            let mut items = Vec::with_capacity(range.end - range.start);
4721                            this.for_each_visible_entry(
4722                                range,
4723                                window,
4724                                cx,
4725                                |id, details, window, cx| {
4726                                    items.push(this.render_entry(id, details, window, cx));
4727                                },
4728                            );
4729                            items
4730                        }
4731                    })
4732                    .when(show_indent_guides, |list| {
4733                        list.with_decoration(
4734                            ui::indent_guides(
4735                                cx.entity().clone(),
4736                                px(indent_size),
4737                                IndentGuideColors::panel(cx),
4738                                |this, range, window, cx| {
4739                                    let mut items =
4740                                        SmallVec::with_capacity(range.end - range.start);
4741                                    this.iter_visible_entries(
4742                                        range,
4743                                        window,
4744                                        cx,
4745                                        |entry, entries, _, _| {
4746                                            let (depth, _) = Self::calculate_depth_and_difference(
4747                                                entry, entries,
4748                                            );
4749                                            items.push(depth);
4750                                        },
4751                                    );
4752                                    items
4753                                },
4754                            )
4755                            .on_click(cx.listener(
4756                                |this, active_indent_guide: &IndentGuideLayout, window, cx| {
4757                                    if window.modifiers().secondary() {
4758                                        let ix = active_indent_guide.offset.y;
4759                                        let Some((target_entry, worktree)) = maybe!({
4760                                            let (worktree_id, entry) = this.entry_at_index(ix)?;
4761                                            let worktree = this
4762                                                .project
4763                                                .read(cx)
4764                                                .worktree_for_id(worktree_id, cx)?;
4765                                            let target_entry = worktree
4766                                                .read(cx)
4767                                                .entry_for_path(&entry.path.parent()?)?;
4768                                            Some((target_entry, worktree))
4769                                        }) else {
4770                                            return;
4771                                        };
4772
4773                                        this.collapse_entry(target_entry.clone(), worktree, cx);
4774                                    }
4775                                },
4776                            ))
4777                            .with_render_fn(
4778                                cx.entity().clone(),
4779                                move |this, params, _, cx| {
4780                                    const LEFT_OFFSET: Pixels = px(14.);
4781                                    const PADDING_Y: Pixels = px(4.);
4782                                    const HITBOX_OVERDRAW: Pixels = px(3.);
4783
4784                                    let active_indent_guide_index =
4785                                        this.find_active_indent_guide(&params.indent_guides, cx);
4786
4787                                    let indent_size = params.indent_size;
4788                                    let item_height = params.item_height;
4789
4790                                    params
4791                                        .indent_guides
4792                                        .into_iter()
4793                                        .enumerate()
4794                                        .map(|(idx, layout)| {
4795                                            let offset = if layout.continues_offscreen {
4796                                                px(0.)
4797                                            } else {
4798                                                PADDING_Y
4799                                            };
4800                                            let bounds = Bounds::new(
4801                                                point(
4802                                                    layout.offset.x * indent_size + LEFT_OFFSET,
4803                                                    layout.offset.y * item_height + offset,
4804                                                ),
4805                                                size(
4806                                                    px(1.),
4807                                                    layout.length * item_height - offset * 2.,
4808                                                ),
4809                                            );
4810                                            ui::RenderedIndentGuide {
4811                                                bounds,
4812                                                layout,
4813                                                is_active: Some(idx) == active_indent_guide_index,
4814                                                hitbox: Some(Bounds::new(
4815                                                    point(
4816                                                        bounds.origin.x - HITBOX_OVERDRAW,
4817                                                        bounds.origin.y,
4818                                                    ),
4819                                                    size(
4820                                                        bounds.size.width + HITBOX_OVERDRAW * 2.,
4821                                                        bounds.size.height,
4822                                                    ),
4823                                                )),
4824                                            }
4825                                        })
4826                                        .collect()
4827                                },
4828                            ),
4829                        )
4830                    })
4831                    .size_full()
4832                    .with_sizing_behavior(ListSizingBehavior::Infer)
4833                    .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4834                    .with_width_from_item(self.max_width_item_index)
4835                    .track_scroll(self.scroll_handle.clone()),
4836                )
4837                .children(self.render_vertical_scrollbar(cx))
4838                .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
4839                    this.pb_4().child(scrollbar)
4840                })
4841                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4842                    deferred(
4843                        anchored()
4844                            .position(*position)
4845                            .anchor(gpui::Corner::TopLeft)
4846                            .child(menu.clone()),
4847                    )
4848                    .with_priority(1)
4849                }))
4850        } else {
4851            v_flex()
4852                .id("empty-project_panel")
4853                .size_full()
4854                .p_4()
4855                .track_focus(&self.focus_handle(cx))
4856                .child(
4857                    Button::new("open_project", "Open a project")
4858                        .full_width()
4859                        .key_binding(KeyBinding::for_action(&workspace::Open, window, cx))
4860                        .on_click(cx.listener(|this, _, window, cx| {
4861                            this.workspace
4862                                .update(cx, |_, cx| {
4863                                    window.dispatch_action(Box::new(workspace::Open), cx)
4864                                })
4865                                .log_err();
4866                        })),
4867                )
4868                .when(is_local, |div| {
4869                    div.drag_over::<ExternalPaths>(|style, _, _, cx| {
4870                        style.bg(cx.theme().colors().drop_target_background)
4871                    })
4872                    .on_drop(cx.listener(
4873                        move |this, external_paths: &ExternalPaths, window, cx| {
4874                            this.last_external_paths_drag_over_entry = None;
4875                            this.marked_entries.clear();
4876                            this.hover_scroll_task.take();
4877                            if let Some(task) = this
4878                                .workspace
4879                                .update(cx, |workspace, cx| {
4880                                    workspace.open_workspace_for_paths(
4881                                        true,
4882                                        external_paths.paths().to_owned(),
4883                                        window,
4884                                        cx,
4885                                    )
4886                                })
4887                                .log_err()
4888                            {
4889                                task.detach_and_log_err(cx);
4890                            }
4891                            cx.stop_propagation();
4892                        },
4893                    ))
4894                })
4895        }
4896    }
4897}
4898
4899impl Render for DraggedProjectEntryView {
4900    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4901        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
4902        h_flex()
4903            .font(ui_font)
4904            .pl(self.click_offset.x + px(12.))
4905            .pt(self.click_offset.y + px(12.))
4906            .child(
4907                div()
4908                    .flex()
4909                    .gap_1()
4910                    .items_center()
4911                    .py_1()
4912                    .px_2()
4913                    .rounded_lg()
4914                    .bg(cx.theme().colors().background)
4915                    .map(|this| {
4916                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
4917                            this.child(Label::new(format!("{} entries", self.selections.len())))
4918                        } else {
4919                            this.child(if let Some(icon) = &self.details.icon {
4920                                div().child(Icon::from_path(icon.clone()))
4921                            } else {
4922                                div()
4923                            })
4924                            .child(Label::new(self.details.filename.clone()))
4925                        }
4926                    }),
4927            )
4928    }
4929}
4930
4931impl EventEmitter<Event> for ProjectPanel {}
4932
4933impl EventEmitter<PanelEvent> for ProjectPanel {}
4934
4935impl Panel for ProjectPanel {
4936    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4937        match ProjectPanelSettings::get_global(cx).dock {
4938            ProjectPanelDockPosition::Left => DockPosition::Left,
4939            ProjectPanelDockPosition::Right => DockPosition::Right,
4940        }
4941    }
4942
4943    fn position_is_valid(&self, position: DockPosition) -> bool {
4944        matches!(position, DockPosition::Left | DockPosition::Right)
4945    }
4946
4947    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4948        settings::update_settings_file::<ProjectPanelSettings>(
4949            self.fs.clone(),
4950            cx,
4951            move |settings, _| {
4952                let dock = match position {
4953                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
4954                    DockPosition::Right => ProjectPanelDockPosition::Right,
4955                };
4956                settings.dock = Some(dock);
4957            },
4958        );
4959    }
4960
4961    fn size(&self, _: &Window, cx: &App) -> Pixels {
4962        self.width
4963            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
4964    }
4965
4966    fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
4967        self.width = size;
4968        self.serialize(cx);
4969        cx.notify();
4970    }
4971
4972    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4973        ProjectPanelSettings::get_global(cx)
4974            .button
4975            .then_some(IconName::FileTree)
4976    }
4977
4978    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
4979        Some("Project Panel")
4980    }
4981
4982    fn toggle_action(&self) -> Box<dyn Action> {
4983        Box::new(ToggleFocus)
4984    }
4985
4986    fn persistent_name() -> &'static str {
4987        "Project Panel"
4988    }
4989
4990    fn starts_open(&self, _: &Window, cx: &App) -> bool {
4991        let project = &self.project.read(cx);
4992        project.visible_worktrees(cx).any(|tree| {
4993            tree.read(cx)
4994                .root_entry()
4995                .map_or(false, |entry| entry.is_dir())
4996        })
4997    }
4998
4999    fn activation_priority(&self) -> u32 {
5000        0
5001    }
5002}
5003
5004impl Focusable for ProjectPanel {
5005    fn focus_handle(&self, _cx: &App) -> FocusHandle {
5006        self.focus_handle.clone()
5007    }
5008}
5009
5010impl ClipboardEntry {
5011    fn is_cut(&self) -> bool {
5012        matches!(self, Self::Cut { .. })
5013    }
5014
5015    fn items(&self) -> &BTreeSet<SelectedEntry> {
5016        match self {
5017            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
5018        }
5019    }
5020}
5021
5022#[cfg(test)]
5023mod project_panel_tests;