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