project_panel.rs

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