project_panel.rs

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