project_panel.rs

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