project_panel.rs

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