project_panel.rs

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