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