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