project_panel.rs

   1mod project_panel_settings;
   2
   3use client::{ErrorCode, ErrorExt};
   4use language::DiagnosticSeverity;
   5use settings::{Settings, SettingsStore};
   6
   7use db::kvp::KEY_VALUE_STORE;
   8use editor::{
   9    items::{
  10        entry_diagnostic_aware_icon_decoration_and_color,
  11        entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color,
  12    },
  13    scroll::{Autoscroll, ScrollbarAutoHide},
  14    Editor, EditorEvent, EditorSettings, ShowScrollbar,
  15};
  16use file_icons::FileIcons;
  17
  18use anyhow::{anyhow, Context as _, Result};
  19use collections::{hash_map, BTreeSet, HashMap};
  20use git::repository::GitFileStatus;
  21use gpui::{
  22    actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action,
  23    AnyElement, AppContext, AssetSource, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent,
  24    Div, DragMoveEvent, EventEmitter, ExternalPaths, FocusHandle, FocusableView, Hsla,
  25    InteractiveElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, Model,
  26    MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy,
  27    Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext,
  28    VisualContext as _, WeakView, WindowContext,
  29};
  30use indexmap::IndexMap;
  31use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
  32use project::{
  33    relativize_path, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree,
  34    WorktreeId,
  35};
  36use project_panel_settings::{
  37    ProjectPanelDockPosition, ProjectPanelSettings, ShowDiagnostics, ShowIndentGuides,
  38};
  39use serde::{Deserialize, Serialize};
  40use smallvec::SmallVec;
  41use std::{
  42    cell::OnceCell,
  43    collections::HashSet,
  44    ffi::OsStr,
  45    ops::Range,
  46    path::{Path, PathBuf},
  47    sync::Arc,
  48    time::Duration,
  49};
  50use theme::ThemeSettings;
  51use ui::{
  52    prelude::*, v_flex, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind,
  53    IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, Scrollbar, ScrollbarState,
  54    Tooltip,
  55};
  56use util::{maybe, ResultExt, TryFutureExt};
  57use workspace::{
  58    dock::{DockPosition, Panel, PanelEvent},
  59    notifications::{DetachAndPromptErr, NotifyTaskExt},
  60    DraggedSelection, OpenInTerminal, PreviewTabsSettings, SelectedEntry, Workspace,
  61};
  62use worktree::CreatedEntry;
  63
  64const PROJECT_PANEL_KEY: &str = "ProjectPanel";
  65const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
  66
  67pub struct ProjectPanel {
  68    project: Model<Project>,
  69    fs: Arc<dyn Fs>,
  70    focus_handle: FocusHandle,
  71    scroll_handle: UniformListScrollHandle,
  72    // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's
  73    // hovered over the start/end of a list.
  74    hover_scroll_task: Option<Task<()>>,
  75    visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
  76    /// Maps from leaf project entry ID to the currently selected ancestor.
  77    /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several
  78    /// project entries (and all non-leaf nodes are guaranteed to be directories).
  79    ancestors: HashMap<ProjectEntryId, FoldedAncestors>,
  80    last_worktree_root_id: Option<ProjectEntryId>,
  81    last_external_paths_drag_over_entry: Option<ProjectEntryId>,
  82    expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
  83    unfolded_dir_ids: HashSet<ProjectEntryId>,
  84    // Currently selected leaf entry (see auto-folding for a definition of that) in a file tree
  85    selection: Option<SelectedEntry>,
  86    marked_entries: BTreeSet<SelectedEntry>,
  87    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
  88    edit_state: Option<EditState>,
  89    filename_editor: View<Editor>,
  90    clipboard: Option<ClipboardEntry>,
  91    _dragged_entry_destination: Option<Arc<Path>>,
  92    workspace: WeakView<Workspace>,
  93    width: Option<Pixels>,
  94    pending_serialization: Task<Option<()>>,
  95    show_scrollbar: bool,
  96    vertical_scrollbar_state: ScrollbarState,
  97    horizontal_scrollbar_state: ScrollbarState,
  98    hide_scrollbar_task: Option<Task<()>>,
  99    diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
 100    max_width_item_index: Option<usize>,
 101    // We keep track of the mouse down state on entries so we don't flash the UI
 102    // in case a user clicks to open a file.
 103    mouse_down: bool,
 104}
 105
 106#[derive(Clone, Debug)]
 107struct EditState {
 108    worktree_id: WorktreeId,
 109    entry_id: ProjectEntryId,
 110    leaf_entry_id: Option<ProjectEntryId>,
 111    is_dir: bool,
 112    depth: usize,
 113    processing_filename: Option<String>,
 114    previously_focused: Option<SelectedEntry>,
 115}
 116
 117impl EditState {
 118    fn is_new_entry(&self) -> bool {
 119        self.leaf_entry_id.is_none()
 120    }
 121}
 122
 123#[derive(Clone, Debug)]
 124enum ClipboardEntry {
 125    Copied(BTreeSet<SelectedEntry>),
 126    Cut(BTreeSet<SelectedEntry>),
 127}
 128
 129#[derive(Debug, PartialEq, Eq, Clone)]
 130struct EntryDetails {
 131    filename: String,
 132    icon: Option<SharedString>,
 133    path: Arc<Path>,
 134    depth: usize,
 135    kind: EntryKind,
 136    is_ignored: bool,
 137    is_expanded: bool,
 138    is_selected: bool,
 139    is_marked: bool,
 140    is_editing: bool,
 141    is_processing: bool,
 142    is_cut: bool,
 143    filename_text_color: Color,
 144    diagnostic_severity: Option<DiagnosticSeverity>,
 145    git_status: Option<GitFileStatus>,
 146    is_private: bool,
 147    worktree_id: WorktreeId,
 148    canonical_path: Option<Box<Path>>,
 149}
 150
 151#[derive(PartialEq, Clone, Default, Debug, Deserialize)]
 152struct Delete {
 153    #[serde(default)]
 154    pub skip_prompt: bool,
 155}
 156
 157#[derive(PartialEq, Clone, Default, Debug, Deserialize)]
 158struct Trash {
 159    #[serde(default)]
 160    pub skip_prompt: bool,
 161}
 162
 163impl_actions!(project_panel, [Delete, Trash]);
 164
 165actions!(
 166    project_panel,
 167    [
 168        ExpandSelectedEntry,
 169        CollapseSelectedEntry,
 170        CollapseAllEntries,
 171        NewDirectory,
 172        NewFile,
 173        Copy,
 174        CopyPath,
 175        CopyRelativePath,
 176        Duplicate,
 177        RevealInFileManager,
 178        RemoveFromProject,
 179        OpenWithSystem,
 180        Cut,
 181        Paste,
 182        Rename,
 183        Open,
 184        OpenPermanent,
 185        ToggleFocus,
 186        NewSearchInDirectory,
 187        UnfoldDirectory,
 188        FoldDirectory,
 189        SelectParent,
 190    ]
 191);
 192
 193#[derive(Debug, Default)]
 194struct FoldedAncestors {
 195    current_ancestor_depth: usize,
 196    ancestors: Vec<ProjectEntryId>,
 197}
 198
 199impl FoldedAncestors {
 200    fn max_ancestor_depth(&self) -> usize {
 201        self.ancestors.len()
 202    }
 203}
 204
 205pub fn init_settings(cx: &mut AppContext) {
 206    ProjectPanelSettings::register(cx);
 207}
 208
 209pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
 210    init_settings(cx);
 211    file_icons::init(assets, cx);
 212
 213    cx.observe_new_views(|workspace: &mut Workspace, _| {
 214        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
 215            workspace.toggle_panel_focus::<ProjectPanel>(cx);
 216        });
 217    })
 218    .detach();
 219}
 220
 221#[derive(Debug)]
 222pub enum Event {
 223    OpenedEntry {
 224        entry_id: ProjectEntryId,
 225        focus_opened_item: bool,
 226        allow_preview: bool,
 227    },
 228    SplitEntry {
 229        entry_id: ProjectEntryId,
 230    },
 231    Focus,
 232}
 233
 234#[derive(Serialize, Deserialize)]
 235struct SerializedProjectPanel {
 236    width: Option<Pixels>,
 237}
 238
 239struct DraggedProjectEntryView {
 240    selection: SelectedEntry,
 241    details: EntryDetails,
 242    width: Pixels,
 243    selections: Arc<BTreeSet<SelectedEntry>>,
 244}
 245
 246struct ItemColors {
 247    default: Hsla,
 248    hover: Hsla,
 249    drag_over: Hsla,
 250    selected: Hsla,
 251    marked_active: Hsla,
 252}
 253
 254fn get_item_color(cx: &ViewContext<ProjectPanel>) -> ItemColors {
 255    let colors = cx.theme().colors();
 256
 257    ItemColors {
 258        default: colors.surface_background,
 259        hover: colors.ghost_element_hover,
 260        drag_over: colors.drop_target_background,
 261        selected: colors.surface_background,
 262        marked_active: colors.ghost_element_selected,
 263    }
 264}
 265
 266impl ProjectPanel {
 267    fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 268        let project = workspace.project().clone();
 269        let project_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
 270            let focus_handle = cx.focus_handle();
 271            cx.on_focus(&focus_handle, Self::focus_in).detach();
 272            cx.on_focus_out(&focus_handle, |this, _, cx| {
 273                this.hide_scrollbar(cx);
 274            })
 275            .detach();
 276            cx.subscribe(&project, |this, project, event, cx| match event {
 277                project::Event::ActiveEntryChanged(Some(entry_id)) => {
 278                    if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
 279                        this.reveal_entry(project, *entry_id, true, cx);
 280                    }
 281                }
 282                project::Event::RevealInProjectPanel(entry_id) => {
 283                    this.reveal_entry(project, *entry_id, false, cx);
 284                    cx.emit(PanelEvent::Activate);
 285                }
 286                project::Event::ActivateProjectPanel => {
 287                    cx.emit(PanelEvent::Activate);
 288                }
 289                project::Event::DiskBasedDiagnosticsFinished { .. }
 290                | project::Event::DiagnosticsUpdated { .. } => {
 291                    if ProjectPanelSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off
 292                    {
 293                        this.update_diagnostics(cx);
 294                        cx.notify();
 295                    }
 296                }
 297                project::Event::WorktreeRemoved(id) => {
 298                    this.expanded_dir_ids.remove(id);
 299                    this.update_visible_entries(None, cx);
 300                    cx.notify();
 301                }
 302                project::Event::WorktreeUpdatedEntries(_, _)
 303                | project::Event::WorktreeAdded
 304                | project::Event::WorktreeOrderChanged => {
 305                    this.update_visible_entries(None, cx);
 306                    cx.notify();
 307                }
 308                _ => {}
 309            })
 310            .detach();
 311
 312            let filename_editor = cx.new_view(Editor::single_line);
 313
 314            cx.subscribe(
 315                &filename_editor,
 316                |project_panel, _, editor_event, cx| match editor_event {
 317                    EditorEvent::BufferEdited | EditorEvent::SelectionsChanged { .. } => {
 318                        project_panel.autoscroll(cx);
 319                    }
 320                    EditorEvent::Blurred => {
 321                        if project_panel
 322                            .edit_state
 323                            .as_ref()
 324                            .map_or(false, |state| state.processing_filename.is_none())
 325                        {
 326                            project_panel.edit_state = None;
 327                            project_panel.update_visible_entries(None, cx);
 328                            cx.notify();
 329                        }
 330                    }
 331                    _ => {}
 332                },
 333            )
 334            .detach();
 335
 336            cx.observe_global::<FileIcons>(|_, cx| {
 337                cx.notify();
 338            })
 339            .detach();
 340
 341            let mut project_panel_settings = *ProjectPanelSettings::get_global(cx);
 342            cx.observe_global::<SettingsStore>(move |this, cx| {
 343                let new_settings = *ProjectPanelSettings::get_global(cx);
 344                if project_panel_settings != new_settings {
 345                    project_panel_settings = new_settings;
 346                    this.update_diagnostics(cx);
 347                    cx.notify();
 348                }
 349            })
 350            .detach();
 351
 352            let scroll_handle = UniformListScrollHandle::new();
 353            let mut this = Self {
 354                project: project.clone(),
 355                hover_scroll_task: None,
 356                fs: workspace.app_state().fs.clone(),
 357                focus_handle,
 358                visible_entries: Default::default(),
 359                ancestors: Default::default(),
 360                last_worktree_root_id: Default::default(),
 361                last_external_paths_drag_over_entry: None,
 362                expanded_dir_ids: Default::default(),
 363                unfolded_dir_ids: Default::default(),
 364                selection: None,
 365                marked_entries: Default::default(),
 366                edit_state: None,
 367                context_menu: None,
 368                filename_editor,
 369                clipboard: None,
 370                _dragged_entry_destination: None,
 371                workspace: workspace.weak_handle(),
 372                width: None,
 373                pending_serialization: Task::ready(None),
 374                show_scrollbar: !Self::should_autohide_scrollbar(cx),
 375                hide_scrollbar_task: None,
 376                vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
 377                    .parent_view(cx.view()),
 378                horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
 379                    .parent_view(cx.view()),
 380                max_width_item_index: None,
 381                diagnostics: Default::default(),
 382                scroll_handle,
 383                mouse_down: false,
 384            };
 385            this.update_visible_entries(None, cx);
 386
 387            this
 388        });
 389
 390        cx.subscribe(&project_panel, {
 391            let project_panel = project_panel.downgrade();
 392            move |workspace, _, event, cx| match event {
 393                &Event::OpenedEntry {
 394                    entry_id,
 395                    focus_opened_item,
 396                    allow_preview,
 397                } => {
 398                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
 399                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 400                            let file_path = entry.path.clone();
 401                            let worktree_id = worktree.read(cx).id();
 402                            let entry_id = entry.id;
 403                            let is_via_ssh = project.read(cx).is_via_ssh();
 404
 405                            workspace
 406                                .open_path_preview(
 407                                    ProjectPath {
 408                                        worktree_id,
 409                                        path: file_path.clone(),
 410                                    },
 411                                    None,
 412                                    focus_opened_item,
 413                                    allow_preview,
 414                                    cx,
 415                                )
 416                                .detach_and_prompt_err("Failed to open file", cx, move |e, _| {
 417                                    match e.error_code() {
 418                                        ErrorCode::Disconnected => if is_via_ssh {
 419                                            Some("Disconnected from SSH host".to_string())
 420                                        } else {
 421                                            Some("Disconnected from remote project".to_string())
 422                                        },
 423                                        ErrorCode::UnsharedItem => Some(format!(
 424                                            "{} is not shared by the host. This could be because it has been marked as `private`",
 425                                            file_path.display()
 426                                        )),
 427                                        _ => None,
 428                                    }
 429                                });
 430
 431                            if let Some(project_panel) = project_panel.upgrade() {
 432                                // Always select and mark the entry, regardless of whether it is opened or not.
 433                                project_panel.update(cx, |project_panel, _| {
 434                                    let entry = SelectedEntry { worktree_id, entry_id };
 435                                    project_panel.marked_entries.clear();
 436                                    project_panel.marked_entries.insert(entry);
 437                                    project_panel.selection = Some(entry);
 438                                });
 439                                if !focus_opened_item {
 440                                    let focus_handle = project_panel.read(cx).focus_handle.clone();
 441                                    cx.focus(&focus_handle);
 442                                }
 443                            }
 444                        }
 445                    }
 446                }
 447                &Event::SplitEntry { entry_id } => {
 448                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
 449                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 450                            workspace
 451                                .split_path(
 452                                    ProjectPath {
 453                                        worktree_id: worktree.read(cx).id(),
 454                                        path: entry.path.clone(),
 455                                    },
 456                                    cx,
 457                                )
 458                                .detach_and_log_err(cx);
 459                        }
 460                    }
 461                }
 462                _ => {}
 463            }
 464        })
 465        .detach();
 466
 467        project_panel
 468    }
 469
 470    pub async fn load(
 471        workspace: WeakView<Workspace>,
 472        mut cx: AsyncWindowContext,
 473    ) -> Result<View<Self>> {
 474        let serialized_panel = cx
 475            .background_executor()
 476            .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
 477            .await
 478            .map_err(|e| anyhow!("Failed to load project panel: {}", e))
 479            .log_err()
 480            .flatten()
 481            .map(|panel| serde_json::from_str::<SerializedProjectPanel>(&panel))
 482            .transpose()
 483            .log_err()
 484            .flatten();
 485
 486        workspace.update(&mut cx, |workspace, cx| {
 487            let panel = ProjectPanel::new(workspace, cx);
 488            if let Some(serialized_panel) = serialized_panel {
 489                panel.update(cx, |panel, cx| {
 490                    panel.width = serialized_panel.width.map(|px| px.round());
 491                    cx.notify();
 492                });
 493            }
 494            panel
 495        })
 496    }
 497
 498    fn update_diagnostics(&mut self, cx: &mut ViewContext<Self>) {
 499        let mut diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity> =
 500            Default::default();
 501        let show_diagnostics_setting = ProjectPanelSettings::get_global(cx).show_diagnostics;
 502
 503        if show_diagnostics_setting != ShowDiagnostics::Off {
 504            self.project
 505                .read(cx)
 506                .diagnostic_summaries(false, cx)
 507                .filter_map(|(path, _, diagnostic_summary)| {
 508                    if diagnostic_summary.error_count > 0 {
 509                        Some((path, DiagnosticSeverity::ERROR))
 510                    } else if show_diagnostics_setting == ShowDiagnostics::All
 511                        && diagnostic_summary.warning_count > 0
 512                    {
 513                        Some((path, DiagnosticSeverity::WARNING))
 514                    } else {
 515                        None
 516                    }
 517                })
 518                .for_each(|(project_path, diagnostic_severity)| {
 519                    let mut path_buffer = PathBuf::new();
 520                    Self::update_strongest_diagnostic_severity(
 521                        &mut diagnostics,
 522                        &project_path,
 523                        path_buffer.clone(),
 524                        diagnostic_severity,
 525                    );
 526
 527                    for component in project_path.path.components() {
 528                        path_buffer.push(component);
 529                        Self::update_strongest_diagnostic_severity(
 530                            &mut diagnostics,
 531                            &project_path,
 532                            path_buffer.clone(),
 533                            diagnostic_severity,
 534                        );
 535                    }
 536                });
 537        }
 538        self.diagnostics = diagnostics;
 539    }
 540
 541    fn update_strongest_diagnostic_severity(
 542        diagnostics: &mut HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
 543        project_path: &ProjectPath,
 544        path_buffer: PathBuf,
 545        diagnostic_severity: DiagnosticSeverity,
 546    ) {
 547        diagnostics
 548            .entry((project_path.worktree_id, path_buffer.clone()))
 549            .and_modify(|strongest_diagnostic_severity| {
 550                *strongest_diagnostic_severity =
 551                    std::cmp::min(*strongest_diagnostic_severity, diagnostic_severity);
 552            })
 553            .or_insert(diagnostic_severity);
 554    }
 555
 556    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 557        let width = self.width;
 558        self.pending_serialization = cx.background_executor().spawn(
 559            async move {
 560                KEY_VALUE_STORE
 561                    .write_kvp(
 562                        PROJECT_PANEL_KEY.into(),
 563                        serde_json::to_string(&SerializedProjectPanel { width })?,
 564                    )
 565                    .await?;
 566                anyhow::Ok(())
 567            }
 568            .log_err(),
 569        );
 570    }
 571
 572    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 573        if !self.focus_handle.contains_focused(cx) {
 574            cx.emit(Event::Focus);
 575        }
 576    }
 577
 578    fn deploy_context_menu(
 579        &mut self,
 580        position: Point<Pixels>,
 581        entry_id: ProjectEntryId,
 582        cx: &mut ViewContext<Self>,
 583    ) {
 584        let project = self.project.read(cx);
 585
 586        let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
 587            id
 588        } else {
 589            return;
 590        };
 591
 592        self.selection = Some(SelectedEntry {
 593            worktree_id,
 594            entry_id,
 595        });
 596
 597        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
 598            let auto_fold_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
 599            let worktree = worktree.read(cx);
 600            let is_root = Some(entry) == worktree.root_entry();
 601            let is_dir = entry.is_dir();
 602            let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
 603            let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
 604            let is_read_only = project.is_read_only(cx);
 605            let is_remote = project.is_via_collab();
 606            let is_local = project.is_local();
 607
 608            let context_menu = ContextMenu::build(cx, |menu, _| {
 609                menu.context(self.focus_handle.clone()).map(|menu| {
 610                    if is_read_only {
 611                        menu.when(is_dir, |menu| {
 612                            menu.action("Search Inside", Box::new(NewSearchInDirectory))
 613                        })
 614                    } else {
 615                        menu.action("New File", Box::new(NewFile))
 616                            .action("New Folder", Box::new(NewDirectory))
 617                            .separator()
 618                            .when(is_local && cfg!(target_os = "macos"), |menu| {
 619                                menu.action("Reveal in Finder", Box::new(RevealInFileManager))
 620                            })
 621                            .when(is_local && cfg!(not(target_os = "macos")), |menu| {
 622                                menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
 623                            })
 624                            .when(is_local, |menu| {
 625                                menu.action("Open in Default App", Box::new(OpenWithSystem))
 626                            })
 627                            .action("Open in Terminal", Box::new(OpenInTerminal))
 628                            .when(is_dir, |menu| {
 629                                menu.separator()
 630                                    .action("Find in Folder…", Box::new(NewSearchInDirectory))
 631                            })
 632                            .when(is_unfoldable, |menu| {
 633                                menu.action("Unfold Directory", Box::new(UnfoldDirectory))
 634                            })
 635                            .when(is_foldable, |menu| {
 636                                menu.action("Fold Directory", Box::new(FoldDirectory))
 637                            })
 638                            .separator()
 639                            .action("Cut", Box::new(Cut))
 640                            .action("Copy", Box::new(Copy))
 641                            .action("Duplicate", Box::new(Duplicate))
 642                            // TODO: Paste should always be visible, cbut disabled when clipboard is empty
 643                            .map(|menu| {
 644                                if self.clipboard.as_ref().is_some() {
 645                                    menu.action("Paste", Box::new(Paste))
 646                                } else {
 647                                    menu.disabled_action("Paste", Box::new(Paste))
 648                                }
 649                            })
 650                            .separator()
 651                            .action("Copy Path", Box::new(CopyPath))
 652                            .action("Copy Relative Path", Box::new(CopyRelativePath))
 653                            .separator()
 654                            .action("Rename", Box::new(Rename))
 655                            .when(!is_root, |menu| {
 656                                menu.action("Trash", Box::new(Trash { skip_prompt: false }))
 657                                    .action("Delete", Box::new(Delete { skip_prompt: false }))
 658                            })
 659                            .when(!is_remote & is_root, |menu| {
 660                                menu.separator()
 661                                    .action(
 662                                        "Add Folder to Project…",
 663                                        Box::new(workspace::AddFolderToProject),
 664                                    )
 665                                    .action("Remove from Project", Box::new(RemoveFromProject))
 666                            })
 667                            .when(is_root, |menu| {
 668                                menu.separator()
 669                                    .action("Collapse All", Box::new(CollapseAllEntries))
 670                            })
 671                    }
 672                })
 673            });
 674
 675            cx.focus_view(&context_menu);
 676            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
 677                this.context_menu.take();
 678                cx.notify();
 679            });
 680            self.context_menu = Some((context_menu, position, subscription));
 681        }
 682
 683        cx.notify();
 684    }
 685
 686    fn is_unfoldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
 687        if !entry.is_dir() || self.unfolded_dir_ids.contains(&entry.id) {
 688            return false;
 689        }
 690
 691        if let Some(parent_path) = entry.path.parent() {
 692            let snapshot = worktree.snapshot();
 693            let mut child_entries = snapshot.child_entries(parent_path);
 694            if let Some(child) = child_entries.next() {
 695                if child_entries.next().is_none() {
 696                    return child.kind.is_dir();
 697                }
 698            }
 699        };
 700        false
 701    }
 702
 703    fn is_foldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
 704        if entry.is_dir() {
 705            let snapshot = worktree.snapshot();
 706
 707            let mut child_entries = snapshot.child_entries(&entry.path);
 708            if let Some(child) = child_entries.next() {
 709                if child_entries.next().is_none() {
 710                    return child.kind.is_dir();
 711                }
 712            }
 713        }
 714        false
 715    }
 716
 717    fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
 718        if let Some((worktree, entry)) = self.selected_entry(cx) {
 719            if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) {
 720                if folded_ancestors.current_ancestor_depth > 0 {
 721                    folded_ancestors.current_ancestor_depth -= 1;
 722                    cx.notify();
 723                    return;
 724                }
 725            }
 726            if entry.is_dir() {
 727                let worktree_id = worktree.id();
 728                let entry_id = entry.id;
 729                let expanded_dir_ids =
 730                    if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
 731                        expanded_dir_ids
 732                    } else {
 733                        return;
 734                    };
 735
 736                match expanded_dir_ids.binary_search(&entry_id) {
 737                    Ok(_) => self.select_next(&SelectNext, cx),
 738                    Err(ix) => {
 739                        self.project.update(cx, |project, cx| {
 740                            project.expand_entry(worktree_id, entry_id, cx);
 741                        });
 742
 743                        expanded_dir_ids.insert(ix, entry_id);
 744                        self.update_visible_entries(None, cx);
 745                        cx.notify();
 746                    }
 747                }
 748            }
 749        }
 750    }
 751
 752    fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
 753        let Some((worktree, entry)) = self.selected_entry_handle(cx) else {
 754            return;
 755        };
 756        self.collapse_entry(entry.clone(), worktree, cx)
 757    }
 758
 759    fn collapse_entry(
 760        &mut self,
 761        entry: Entry,
 762        worktree: Model<Worktree>,
 763        cx: &mut ViewContext<Self>,
 764    ) {
 765        let worktree = worktree.read(cx);
 766        if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) {
 767            if folded_ancestors.current_ancestor_depth + 1 < folded_ancestors.max_ancestor_depth() {
 768                folded_ancestors.current_ancestor_depth += 1;
 769                cx.notify();
 770                return;
 771            }
 772        }
 773        let worktree_id = worktree.id();
 774        let expanded_dir_ids =
 775            if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
 776                expanded_dir_ids
 777            } else {
 778                return;
 779            };
 780
 781        let mut entry = &entry;
 782        loop {
 783            let entry_id = entry.id;
 784            match expanded_dir_ids.binary_search(&entry_id) {
 785                Ok(ix) => {
 786                    expanded_dir_ids.remove(ix);
 787                    self.update_visible_entries(Some((worktree_id, entry_id)), cx);
 788                    cx.notify();
 789                    break;
 790                }
 791                Err(_) => {
 792                    if let Some(parent_entry) =
 793                        entry.path.parent().and_then(|p| worktree.entry_for_path(p))
 794                    {
 795                        entry = parent_entry;
 796                    } else {
 797                        break;
 798                    }
 799                }
 800            }
 801        }
 802    }
 803
 804    pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
 805        // By keeping entries for fully collapsed worktrees, we avoid expanding them within update_visible_entries
 806        // (which is it's default behavior when there's no entry for a worktree in expanded_dir_ids).
 807        self.expanded_dir_ids
 808            .retain(|_, expanded_entries| expanded_entries.is_empty());
 809        self.update_visible_entries(None, cx);
 810        cx.notify();
 811    }
 812
 813    fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
 814        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
 815            if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
 816                self.project.update(cx, |project, cx| {
 817                    match expanded_dir_ids.binary_search(&entry_id) {
 818                        Ok(ix) => {
 819                            expanded_dir_ids.remove(ix);
 820                        }
 821                        Err(ix) => {
 822                            project.expand_entry(worktree_id, entry_id, cx);
 823                            expanded_dir_ids.insert(ix, entry_id);
 824                        }
 825                    }
 826                });
 827                self.update_visible_entries(Some((worktree_id, entry_id)), cx);
 828                cx.focus(&self.focus_handle);
 829                cx.notify();
 830            }
 831        }
 832    }
 833
 834    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
 835        if let Some(edit_state) = &self.edit_state {
 836            if edit_state.processing_filename.is_none() {
 837                self.filename_editor.update(cx, |editor, cx| {
 838                    editor.move_to_beginning_of_line(
 839                        &editor::actions::MoveToBeginningOfLine {
 840                            stop_at_soft_wraps: false,
 841                        },
 842                        cx,
 843                    );
 844                });
 845                return;
 846            }
 847        }
 848        if let Some(selection) = self.selection {
 849            let (mut worktree_ix, mut entry_ix, _) =
 850                self.index_for_selection(selection).unwrap_or_default();
 851            if entry_ix > 0 {
 852                entry_ix -= 1;
 853            } else if worktree_ix > 0 {
 854                worktree_ix -= 1;
 855                entry_ix = self.visible_entries[worktree_ix].1.len() - 1;
 856            } else {
 857                return;
 858            }
 859
 860            let (worktree_id, worktree_entries, _) = &self.visible_entries[worktree_ix];
 861            let selection = SelectedEntry {
 862                worktree_id: *worktree_id,
 863                entry_id: worktree_entries[entry_ix].id,
 864            };
 865            self.selection = Some(selection);
 866            if cx.modifiers().shift {
 867                self.marked_entries.insert(selection);
 868            }
 869            self.autoscroll(cx);
 870            cx.notify();
 871        } else {
 872            self.select_first(&SelectFirst {}, cx);
 873        }
 874    }
 875
 876    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
 877        if let Some(task) = self.confirm_edit(cx) {
 878            task.detach_and_notify_err(cx);
 879        }
 880    }
 881
 882    fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
 883        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
 884        self.open_internal(true, !preview_tabs_enabled, cx);
 885    }
 886
 887    fn open_permanent(&mut self, _: &OpenPermanent, cx: &mut ViewContext<Self>) {
 888        self.open_internal(false, true, cx);
 889    }
 890
 891    fn open_internal(
 892        &mut self,
 893        allow_preview: bool,
 894        focus_opened_item: bool,
 895        cx: &mut ViewContext<Self>,
 896    ) {
 897        if let Some((_, entry)) = self.selected_entry(cx) {
 898            if entry.is_file() {
 899                self.open_entry(entry.id, focus_opened_item, allow_preview, cx);
 900            } else {
 901                self.toggle_expanded(entry.id, cx);
 902            }
 903        }
 904    }
 905
 906    fn confirm_edit(&mut self, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
 907        let edit_state = self.edit_state.as_mut()?;
 908        cx.focus(&self.focus_handle);
 909
 910        let worktree_id = edit_state.worktree_id;
 911        let is_new_entry = edit_state.is_new_entry();
 912        let filename = self.filename_editor.read(cx).text(cx);
 913        edit_state.is_dir = edit_state.is_dir
 914            || (edit_state.is_new_entry() && filename.ends_with(std::path::MAIN_SEPARATOR));
 915        let is_dir = edit_state.is_dir;
 916        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
 917        let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
 918
 919        let path_already_exists = |path| worktree.read(cx).entry_for_path(path).is_some();
 920        let edit_task;
 921        let edited_entry_id;
 922        if is_new_entry {
 923            self.selection = Some(SelectedEntry {
 924                worktree_id,
 925                entry_id: NEW_ENTRY_ID,
 926            });
 927            let new_path = entry.path.join(filename.trim_start_matches('/'));
 928            if path_already_exists(new_path.as_path()) {
 929                return None;
 930            }
 931
 932            edited_entry_id = NEW_ENTRY_ID;
 933            edit_task = self.project.update(cx, |project, cx| {
 934                project.create_entry((worktree_id, &new_path), is_dir, cx)
 935            });
 936        } else {
 937            let new_path = if let Some(parent) = entry.path.clone().parent() {
 938                parent.join(&filename)
 939            } else {
 940                filename.clone().into()
 941            };
 942            if path_already_exists(new_path.as_path()) {
 943                return None;
 944            }
 945            edited_entry_id = entry.id;
 946            edit_task = self.project.update(cx, |project, cx| {
 947                project.rename_entry(entry.id, new_path.as_path(), cx)
 948            });
 949        };
 950
 951        edit_state.processing_filename = Some(filename);
 952        cx.notify();
 953
 954        Some(cx.spawn(|project_panel, mut cx| async move {
 955            let new_entry = edit_task.await;
 956            project_panel.update(&mut cx, |project_panel, cx| {
 957                project_panel.edit_state = None;
 958                cx.notify();
 959            })?;
 960
 961            match new_entry {
 962                Err(e) => {
 963                    project_panel.update(&mut cx, |project_panel, cx| {
 964                        project_panel.marked_entries.clear();
 965                        project_panel.update_visible_entries(None, cx);
 966                    }).ok();
 967                    Err(e)?;
 968                }
 969                Ok(CreatedEntry::Included(new_entry)) => {
 970                    project_panel.update(&mut cx, |project_panel, cx| {
 971                        if let Some(selection) = &mut project_panel.selection {
 972                            if selection.entry_id == edited_entry_id {
 973                                selection.worktree_id = worktree_id;
 974                                selection.entry_id = new_entry.id;
 975                                project_panel.marked_entries.clear();
 976                                project_panel.expand_to_selection(cx);
 977                            }
 978                        }
 979                        project_panel.update_visible_entries(None, cx);
 980                        if is_new_entry && !is_dir {
 981                            project_panel.open_entry(new_entry.id, true, false, cx);
 982                        }
 983                        cx.notify();
 984                    })?;
 985                }
 986                Ok(CreatedEntry::Excluded { abs_path }) => {
 987                    if let Some(open_task) = project_panel
 988                        .update(&mut cx, |project_panel, cx| {
 989                            project_panel.marked_entries.clear();
 990                            project_panel.update_visible_entries(None, cx);
 991
 992                            if is_dir {
 993                                project_panel.project.update(cx, |_, cx| {
 994                                    cx.emit(project::Event::Toast {
 995                                        notification_id: "excluded-directory".into(),
 996                                        message: format!("Created an excluded directory at {abs_path:?}.\nAlter `file_scan_exclusions` in the settings to show it in the panel")
 997                                    })
 998                                });
 999                                None
1000                            } else {
1001                                project_panel
1002                                    .workspace
1003                                    .update(cx, |workspace, cx| {
1004                                        workspace.open_abs_path(abs_path, true, cx)
1005                                    })
1006                                    .ok()
1007                            }
1008                        })
1009                        .ok()
1010                        .flatten()
1011                    {
1012                        let _ = open_task.await?;
1013                    }
1014                }
1015            }
1016            Ok(())
1017        }))
1018    }
1019
1020    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1021        let previous_edit_state = self.edit_state.take();
1022        self.update_visible_entries(None, cx);
1023        self.marked_entries.clear();
1024
1025        if let Some(previously_focused) =
1026            previous_edit_state.and_then(|edit_state| edit_state.previously_focused)
1027        {
1028            self.selection = Some(previously_focused);
1029            self.autoscroll(cx);
1030        }
1031
1032        cx.focus(&self.focus_handle);
1033        cx.notify();
1034    }
1035
1036    fn open_entry(
1037        &mut self,
1038        entry_id: ProjectEntryId,
1039        focus_opened_item: bool,
1040        allow_preview: bool,
1041        cx: &mut ViewContext<Self>,
1042    ) {
1043        cx.emit(Event::OpenedEntry {
1044            entry_id,
1045            focus_opened_item,
1046            allow_preview,
1047        });
1048    }
1049
1050    fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
1051        cx.emit(Event::SplitEntry { entry_id });
1052    }
1053
1054    fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext<Self>) {
1055        self.add_entry(false, cx)
1056    }
1057
1058    fn new_directory(&mut self, _: &NewDirectory, cx: &mut ViewContext<Self>) {
1059        self.add_entry(true, cx)
1060    }
1061
1062    fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext<Self>) {
1063        if let Some(SelectedEntry {
1064            worktree_id,
1065            entry_id,
1066        }) = self.selection
1067        {
1068            let directory_id;
1069            let new_entry_id = self.resolve_entry(entry_id);
1070            if let Some((worktree, expanded_dir_ids)) = self
1071                .project
1072                .read(cx)
1073                .worktree_for_id(worktree_id, cx)
1074                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
1075            {
1076                let worktree = worktree.read(cx);
1077                if let Some(mut entry) = worktree.entry_for_id(new_entry_id) {
1078                    loop {
1079                        if entry.is_dir() {
1080                            if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1081                                expanded_dir_ids.insert(ix, entry.id);
1082                            }
1083                            directory_id = entry.id;
1084                            break;
1085                        } else {
1086                            if let Some(parent_path) = entry.path.parent() {
1087                                if let Some(parent_entry) = worktree.entry_for_path(parent_path) {
1088                                    entry = parent_entry;
1089                                    continue;
1090                                }
1091                            }
1092                            return;
1093                        }
1094                    }
1095                } else {
1096                    return;
1097                };
1098            } else {
1099                return;
1100            };
1101            self.marked_entries.clear();
1102            self.edit_state = Some(EditState {
1103                worktree_id,
1104                entry_id: directory_id,
1105                leaf_entry_id: None,
1106                is_dir,
1107                processing_filename: None,
1108                previously_focused: self.selection,
1109                depth: 0,
1110            });
1111            self.filename_editor.update(cx, |editor, cx| {
1112                editor.clear(cx);
1113                editor.focus(cx);
1114            });
1115            self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx);
1116            self.autoscroll(cx);
1117            cx.notify();
1118        }
1119    }
1120
1121    fn unflatten_entry_id(&self, leaf_entry_id: ProjectEntryId) -> ProjectEntryId {
1122        if let Some(ancestors) = self.ancestors.get(&leaf_entry_id) {
1123            ancestors
1124                .ancestors
1125                .get(ancestors.current_ancestor_depth)
1126                .copied()
1127                .unwrap_or(leaf_entry_id)
1128        } else {
1129            leaf_entry_id
1130        }
1131    }
1132
1133    fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) {
1134        if let Some(SelectedEntry {
1135            worktree_id,
1136            entry_id,
1137        }) = self.selection
1138        {
1139            if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) {
1140                let sub_entry_id = self.unflatten_entry_id(entry_id);
1141                if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) {
1142                    self.edit_state = Some(EditState {
1143                        worktree_id,
1144                        entry_id: sub_entry_id,
1145                        leaf_entry_id: Some(entry_id),
1146                        is_dir: entry.is_dir(),
1147                        processing_filename: None,
1148                        previously_focused: None,
1149                        depth: 0,
1150                    });
1151                    let file_name = entry
1152                        .path
1153                        .file_name()
1154                        .map(|s| s.to_string_lossy())
1155                        .unwrap_or_default()
1156                        .to_string();
1157                    let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
1158                    let selection_end =
1159                        file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
1160                    self.filename_editor.update(cx, |editor, cx| {
1161                        editor.set_text(file_name, cx);
1162                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1163                            s.select_ranges([0..selection_end])
1164                        });
1165                        editor.focus(cx);
1166                    });
1167                    self.update_visible_entries(None, cx);
1168                    self.autoscroll(cx);
1169                    cx.notify();
1170                }
1171            }
1172        }
1173    }
1174
1175    fn trash(&mut self, action: &Trash, cx: &mut ViewContext<Self>) {
1176        self.remove(true, action.skip_prompt, cx);
1177    }
1178
1179    fn delete(&mut self, action: &Delete, cx: &mut ViewContext<Self>) {
1180        self.remove(false, action.skip_prompt, cx);
1181    }
1182
1183    fn remove(&mut self, trash: bool, skip_prompt: bool, cx: &mut ViewContext<'_, ProjectPanel>) {
1184        maybe!({
1185            if self.marked_entries.is_empty() && self.selection.is_none() {
1186                return None;
1187            }
1188            let project = self.project.read(cx);
1189            let items_to_delete = self.marked_entries();
1190
1191            let mut dirty_buffers = 0;
1192            let file_paths = items_to_delete
1193                .into_iter()
1194                .filter_map(|selection| {
1195                    let project_path = project.path_for_entry(selection.entry_id, cx)?;
1196                    dirty_buffers +=
1197                        project.dirty_buffers(cx).any(|path| path == project_path) as usize;
1198                    Some((
1199                        selection.entry_id,
1200                        project_path
1201                            .path
1202                            .file_name()?
1203                            .to_string_lossy()
1204                            .into_owned(),
1205                    ))
1206                })
1207                .collect::<Vec<_>>();
1208            if file_paths.is_empty() {
1209                return None;
1210            }
1211            let answer = if !skip_prompt {
1212                let operation = if trash { "Trash" } else { "Delete" };
1213                let prompt = match file_paths.first() {
1214                    Some((_, path)) if file_paths.len() == 1 => {
1215                        let unsaved_warning = if dirty_buffers > 0 {
1216                            "\n\nIt has unsaved changes, which will be lost."
1217                        } else {
1218                            ""
1219                        };
1220
1221                        format!("{operation} {path}?{unsaved_warning}")
1222                    }
1223                    _ => {
1224                        const CUTOFF_POINT: usize = 10;
1225                        let names = if file_paths.len() > CUTOFF_POINT {
1226                            let truncated_path_counts = file_paths.len() - CUTOFF_POINT;
1227                            let mut paths = file_paths
1228                                .iter()
1229                                .map(|(_, path)| path.clone())
1230                                .take(CUTOFF_POINT)
1231                                .collect::<Vec<_>>();
1232                            paths.truncate(CUTOFF_POINT);
1233                            if truncated_path_counts == 1 {
1234                                paths.push(".. 1 file not shown".into());
1235                            } else {
1236                                paths.push(format!(".. {} files not shown", truncated_path_counts));
1237                            }
1238                            paths
1239                        } else {
1240                            file_paths.iter().map(|(_, path)| path.clone()).collect()
1241                        };
1242                        let unsaved_warning = if dirty_buffers == 0 {
1243                            String::new()
1244                        } else if dirty_buffers == 1 {
1245                            "\n\n1 of these has unsaved changes, which will be lost.".to_string()
1246                        } else {
1247                            format!("\n\n{dirty_buffers} of these have unsaved changes, which will be lost.")
1248                        };
1249
1250                        format!(
1251                            "Do you want to {} the following {} files?\n{}{unsaved_warning}",
1252                            operation.to_lowercase(),
1253                            file_paths.len(),
1254                            names.join("\n")
1255                        )
1256                    }
1257                };
1258                Some(cx.prompt(PromptLevel::Info, &prompt, None, &[operation, "Cancel"]))
1259            } else {
1260                None
1261            };
1262
1263            cx.spawn(|this, mut cx| async move {
1264                if let Some(answer) = answer {
1265                    if answer.await != Ok(0) {
1266                        return Result::<(), anyhow::Error>::Ok(());
1267                    }
1268                }
1269                for (entry_id, _) in file_paths {
1270                    this.update(&mut cx, |this, cx| {
1271                        this.project
1272                            .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
1273                            .ok_or_else(|| anyhow!("no such entry"))
1274                    })??
1275                    .await?;
1276                }
1277                Result::<(), anyhow::Error>::Ok(())
1278            })
1279            .detach_and_log_err(cx);
1280            Some(())
1281        });
1282    }
1283
1284    fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext<Self>) {
1285        if let Some((worktree, entry)) = self.selected_entry(cx) {
1286            self.unfolded_dir_ids.insert(entry.id);
1287
1288            let snapshot = worktree.snapshot();
1289            let mut parent_path = entry.path.parent();
1290            while let Some(path) = parent_path {
1291                if let Some(parent_entry) = worktree.entry_for_path(path) {
1292                    let mut children_iter = snapshot.child_entries(path);
1293
1294                    if children_iter.by_ref().take(2).count() > 1 {
1295                        break;
1296                    }
1297
1298                    self.unfolded_dir_ids.insert(parent_entry.id);
1299                    parent_path = path.parent();
1300                } else {
1301                    break;
1302                }
1303            }
1304
1305            self.update_visible_entries(None, cx);
1306            self.autoscroll(cx);
1307            cx.notify();
1308        }
1309    }
1310
1311    fn fold_directory(&mut self, _: &FoldDirectory, cx: &mut ViewContext<Self>) {
1312        if let Some((worktree, entry)) = self.selected_entry(cx) {
1313            self.unfolded_dir_ids.remove(&entry.id);
1314
1315            let snapshot = worktree.snapshot();
1316            let mut path = &*entry.path;
1317            loop {
1318                let mut child_entries_iter = snapshot.child_entries(path);
1319                if let Some(child) = child_entries_iter.next() {
1320                    if child_entries_iter.next().is_none() && child.is_dir() {
1321                        self.unfolded_dir_ids.remove(&child.id);
1322                        path = &*child.path;
1323                    } else {
1324                        break;
1325                    }
1326                } else {
1327                    break;
1328                }
1329            }
1330
1331            self.update_visible_entries(None, cx);
1332            self.autoscroll(cx);
1333            cx.notify();
1334        }
1335    }
1336
1337    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1338        if let Some(edit_state) = &self.edit_state {
1339            if edit_state.processing_filename.is_none() {
1340                self.filename_editor.update(cx, |editor, cx| {
1341                    editor.move_to_end_of_line(
1342                        &editor::actions::MoveToEndOfLine {
1343                            stop_at_soft_wraps: false,
1344                        },
1345                        cx,
1346                    );
1347                });
1348                return;
1349            }
1350        }
1351        if let Some(selection) = self.selection {
1352            let (mut worktree_ix, mut entry_ix, _) =
1353                self.index_for_selection(selection).unwrap_or_default();
1354            if let Some((_, worktree_entries, _)) = self.visible_entries.get(worktree_ix) {
1355                if entry_ix + 1 < worktree_entries.len() {
1356                    entry_ix += 1;
1357                } else {
1358                    worktree_ix += 1;
1359                    entry_ix = 0;
1360                }
1361            }
1362
1363            if let Some((worktree_id, worktree_entries, _)) = self.visible_entries.get(worktree_ix)
1364            {
1365                if let Some(entry) = worktree_entries.get(entry_ix) {
1366                    let selection = SelectedEntry {
1367                        worktree_id: *worktree_id,
1368                        entry_id: entry.id,
1369                    };
1370                    self.selection = Some(selection);
1371                    if cx.modifiers().shift {
1372                        self.marked_entries.insert(selection);
1373                    }
1374
1375                    self.autoscroll(cx);
1376                    cx.notify();
1377                }
1378            }
1379        } else {
1380            self.select_first(&SelectFirst {}, cx);
1381        }
1382    }
1383
1384    fn select_parent(&mut self, _: &SelectParent, cx: &mut ViewContext<Self>) {
1385        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
1386            if let Some(parent) = entry.path.parent() {
1387                let worktree = worktree.read(cx);
1388                if let Some(parent_entry) = worktree.entry_for_path(parent) {
1389                    self.selection = Some(SelectedEntry {
1390                        worktree_id: worktree.id(),
1391                        entry_id: parent_entry.id,
1392                    });
1393                    self.autoscroll(cx);
1394                    cx.notify();
1395                }
1396            }
1397        } else {
1398            self.select_first(&SelectFirst {}, cx);
1399        }
1400    }
1401
1402    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
1403        let worktree = self
1404            .visible_entries
1405            .first()
1406            .and_then(|(worktree_id, _, _)| {
1407                self.project.read(cx).worktree_for_id(*worktree_id, cx)
1408            });
1409        if let Some(worktree) = worktree {
1410            let worktree = worktree.read(cx);
1411            let worktree_id = worktree.id();
1412            if let Some(root_entry) = worktree.root_entry() {
1413                let selection = SelectedEntry {
1414                    worktree_id,
1415                    entry_id: root_entry.id,
1416                };
1417                self.selection = Some(selection);
1418                if cx.modifiers().shift {
1419                    self.marked_entries.insert(selection);
1420                }
1421                self.autoscroll(cx);
1422                cx.notify();
1423            }
1424        }
1425    }
1426
1427    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
1428        let worktree = self.visible_entries.last().and_then(|(worktree_id, _, _)| {
1429            self.project.read(cx).worktree_for_id(*worktree_id, cx)
1430        });
1431        if let Some(worktree) = worktree {
1432            let worktree = worktree.read(cx);
1433            let worktree_id = worktree.id();
1434            if let Some(last_entry) = worktree.entries(true, 0).last() {
1435                self.selection = Some(SelectedEntry {
1436                    worktree_id,
1437                    entry_id: last_entry.id,
1438                });
1439                self.autoscroll(cx);
1440                cx.notify();
1441            }
1442        }
1443    }
1444
1445    fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
1446        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
1447            self.scroll_handle
1448                .scroll_to_item(index, ScrollStrategy::Center);
1449            cx.notify();
1450        }
1451    }
1452
1453    fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
1454        let entries = self.marked_entries();
1455        if !entries.is_empty() {
1456            self.clipboard = Some(ClipboardEntry::Cut(entries));
1457            cx.notify();
1458        }
1459    }
1460
1461    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
1462        let entries = self.marked_entries();
1463        if !entries.is_empty() {
1464            self.clipboard = Some(ClipboardEntry::Copied(entries));
1465            cx.notify();
1466        }
1467    }
1468
1469    fn create_paste_path(
1470        &self,
1471        source: &SelectedEntry,
1472        (worktree, target_entry): (Model<Worktree>, &Entry),
1473        cx: &AppContext,
1474    ) -> Option<PathBuf> {
1475        let mut new_path = target_entry.path.to_path_buf();
1476        // If we're pasting into a file, or a directory into itself, go up one level.
1477        if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
1478            new_path.pop();
1479        }
1480        let clipboard_entry_file_name = self
1481            .project
1482            .read(cx)
1483            .path_for_entry(source.entry_id, cx)?
1484            .path
1485            .file_name()?
1486            .to_os_string();
1487        new_path.push(&clipboard_entry_file_name);
1488        let extension = new_path.extension().map(|e| e.to_os_string());
1489        let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
1490        let mut ix = 0;
1491        {
1492            let worktree = worktree.read(cx);
1493            while worktree.entry_for_path(&new_path).is_some() {
1494                new_path.pop();
1495
1496                let mut new_file_name = file_name_without_extension.to_os_string();
1497                new_file_name.push(" copy");
1498                if ix > 0 {
1499                    new_file_name.push(format!(" {}", ix));
1500                }
1501                if let Some(extension) = extension.as_ref() {
1502                    new_file_name.push(".");
1503                    new_file_name.push(extension);
1504                }
1505
1506                new_path.push(new_file_name);
1507                ix += 1;
1508            }
1509        }
1510        Some(new_path)
1511    }
1512
1513    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
1514        maybe!({
1515            let (worktree, entry) = self.selected_entry_handle(cx)?;
1516            let entry = entry.clone();
1517            let worktree_id = worktree.read(cx).id();
1518            let clipboard_entries = self
1519                .clipboard
1520                .as_ref()
1521                .filter(|clipboard| !clipboard.items().is_empty())?;
1522            enum PasteTask {
1523                Rename(Task<Result<CreatedEntry>>),
1524                Copy(Task<Result<Option<Entry>>>),
1525            }
1526            let mut paste_entry_tasks: IndexMap<(ProjectEntryId, bool), PasteTask> =
1527                IndexMap::default();
1528            let clip_is_cut = clipboard_entries.is_cut();
1529            for clipboard_entry in clipboard_entries.items() {
1530                let new_path =
1531                    self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
1532                let clip_entry_id = clipboard_entry.entry_id;
1533                let is_same_worktree = clipboard_entry.worktree_id == worktree_id;
1534                let relative_worktree_source_path = if !is_same_worktree {
1535                    let target_base_path = worktree.read(cx).abs_path();
1536                    let clipboard_project_path =
1537                        self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
1538                    let clipboard_abs_path = self
1539                        .project
1540                        .read(cx)
1541                        .absolute_path(&clipboard_project_path, cx)?;
1542                    Some(relativize_path(
1543                        &target_base_path,
1544                        clipboard_abs_path.as_path(),
1545                    ))
1546                } else {
1547                    None
1548                };
1549                let task = if clip_is_cut && is_same_worktree {
1550                    let task = self.project.update(cx, |project, cx| {
1551                        project.rename_entry(clip_entry_id, new_path, cx)
1552                    });
1553                    PasteTask::Rename(task)
1554                } else {
1555                    let entry_id = if is_same_worktree {
1556                        clip_entry_id
1557                    } else {
1558                        entry.id
1559                    };
1560                    let task = self.project.update(cx, |project, cx| {
1561                        project.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
1562                    });
1563                    PasteTask::Copy(task)
1564                };
1565                let needs_delete = !is_same_worktree && clip_is_cut;
1566                paste_entry_tasks.insert((clip_entry_id, needs_delete), task);
1567            }
1568
1569            cx.spawn(|project_panel, mut cx| async move {
1570                let mut last_succeed = None;
1571                let mut need_delete_ids = Vec::new();
1572                for ((entry_id, need_delete), task) in paste_entry_tasks.into_iter() {
1573                    match task {
1574                        PasteTask::Rename(task) => {
1575                            if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
1576                                last_succeed = Some(entry.id);
1577                            }
1578                        }
1579                        PasteTask::Copy(task) => {
1580                            if let Some(Some(entry)) = task.await.log_err() {
1581                                last_succeed = Some(entry.id);
1582                                if need_delete {
1583                                    need_delete_ids.push(entry_id);
1584                                }
1585                            }
1586                        }
1587                    }
1588                }
1589                // update selection
1590                if let Some(entry_id) = last_succeed {
1591                    project_panel
1592                        .update(&mut cx, |project_panel, _cx| {
1593                            project_panel.selection = Some(SelectedEntry {
1594                                worktree_id,
1595                                entry_id,
1596                            });
1597                        })
1598                        .ok();
1599                }
1600                // remove entry for cut in difference worktree
1601                for entry_id in need_delete_ids {
1602                    project_panel
1603                        .update(&mut cx, |project_panel, cx| {
1604                            project_panel
1605                                .project
1606                                .update(cx, |project, cx| project.delete_entry(entry_id, true, cx))
1607                                .ok_or_else(|| anyhow!("no such entry"))
1608                        })??
1609                        .await?;
1610                }
1611
1612                anyhow::Ok(())
1613            })
1614            .detach_and_log_err(cx);
1615
1616            self.expand_entry(worktree_id, entry.id, cx);
1617            Some(())
1618        });
1619    }
1620
1621    fn duplicate(&mut self, _: &Duplicate, cx: &mut ViewContext<Self>) {
1622        self.copy(&Copy {}, cx);
1623        self.paste(&Paste {}, cx);
1624    }
1625
1626    fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
1627        let abs_file_paths = {
1628            let project = self.project.read(cx);
1629            self.marked_entries()
1630                .into_iter()
1631                .filter_map(|entry| {
1632                    let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
1633                    Some(
1634                        project
1635                            .worktree_for_id(entry.worktree_id, cx)?
1636                            .read(cx)
1637                            .abs_path()
1638                            .join(entry_path)
1639                            .to_string_lossy()
1640                            .to_string(),
1641                    )
1642                })
1643                .collect::<Vec<_>>()
1644        };
1645        if !abs_file_paths.is_empty() {
1646            cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
1647        }
1648    }
1649
1650    fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
1651        let file_paths = {
1652            let project = self.project.read(cx);
1653            self.marked_entries()
1654                .into_iter()
1655                .filter_map(|entry| {
1656                    Some(
1657                        project
1658                            .path_for_entry(entry.entry_id, cx)?
1659                            .path
1660                            .to_string_lossy()
1661                            .to_string(),
1662                    )
1663                })
1664                .collect::<Vec<_>>()
1665        };
1666        if !file_paths.is_empty() {
1667            cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
1668        }
1669    }
1670
1671    fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
1672        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
1673            cx.reveal_path(&worktree.read(cx).abs_path().join(&entry.path));
1674        }
1675    }
1676
1677    fn remove_from_project(&mut self, _: &RemoveFromProject, cx: &mut ViewContext<Self>) {
1678        if let Some((worktree, _)) = self.selected_sub_entry(cx) {
1679            let worktree_id = worktree.read(cx).id();
1680            self.project
1681                .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
1682        }
1683    }
1684
1685    fn open_system(&mut self, _: &OpenWithSystem, cx: &mut ViewContext<Self>) {
1686        if let Some((worktree, entry)) = self.selected_entry(cx) {
1687            let abs_path = worktree.abs_path().join(&entry.path);
1688            cx.open_with_system(&abs_path);
1689        }
1690    }
1691
1692    fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
1693        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
1694            let abs_path = match &entry.canonical_path {
1695                Some(canonical_path) => Some(canonical_path.to_path_buf()),
1696                None => worktree.read(cx).absolutize(&entry.path).ok(),
1697            };
1698
1699            let working_directory = if entry.is_dir() {
1700                abs_path
1701            } else {
1702                abs_path.and_then(|path| Some(path.parent()?.to_path_buf()))
1703            };
1704            if let Some(working_directory) = working_directory {
1705                cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone())
1706            }
1707        }
1708    }
1709
1710    pub fn new_search_in_directory(
1711        &mut self,
1712        _: &NewSearchInDirectory,
1713        cx: &mut ViewContext<Self>,
1714    ) {
1715        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
1716            if entry.is_dir() {
1717                let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
1718                let dir_path = if include_root {
1719                    let mut full_path = PathBuf::from(worktree.read(cx).root_name());
1720                    full_path.push(&entry.path);
1721                    Arc::from(full_path)
1722                } else {
1723                    entry.path.clone()
1724                };
1725
1726                self.workspace
1727                    .update(cx, |workspace, cx| {
1728                        search::ProjectSearchView::new_search_in_directory(
1729                            workspace, &dir_path, cx,
1730                        );
1731                    })
1732                    .ok();
1733            }
1734        }
1735    }
1736
1737    fn move_entry(
1738        &mut self,
1739        entry_to_move: ProjectEntryId,
1740        destination: ProjectEntryId,
1741        destination_is_file: bool,
1742        cx: &mut ViewContext<Self>,
1743    ) {
1744        if self
1745            .project
1746            .read(cx)
1747            .entry_is_worktree_root(entry_to_move, cx)
1748        {
1749            self.move_worktree_root(entry_to_move, destination, cx)
1750        } else {
1751            self.move_worktree_entry(entry_to_move, destination, destination_is_file, cx)
1752        }
1753    }
1754
1755    fn move_worktree_root(
1756        &mut self,
1757        entry_to_move: ProjectEntryId,
1758        destination: ProjectEntryId,
1759        cx: &mut ViewContext<Self>,
1760    ) {
1761        self.project.update(cx, |project, cx| {
1762            let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
1763                return;
1764            };
1765            let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
1766                return;
1767            };
1768
1769            let worktree_id = worktree_to_move.read(cx).id();
1770            let destination_id = destination_worktree.read(cx).id();
1771
1772            project
1773                .move_worktree(worktree_id, destination_id, cx)
1774                .log_err();
1775        });
1776    }
1777
1778    fn move_worktree_entry(
1779        &mut self,
1780        entry_to_move: ProjectEntryId,
1781        destination: ProjectEntryId,
1782        destination_is_file: bool,
1783        cx: &mut ViewContext<Self>,
1784    ) {
1785        if entry_to_move == destination {
1786            return;
1787        }
1788
1789        let destination_worktree = self.project.update(cx, |project, cx| {
1790            let entry_path = project.path_for_entry(entry_to_move, cx)?;
1791            let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
1792
1793            let mut destination_path = destination_entry_path.as_ref();
1794            if destination_is_file {
1795                destination_path = destination_path.parent()?;
1796            }
1797
1798            let mut new_path = destination_path.to_path_buf();
1799            new_path.push(entry_path.path.file_name()?);
1800            if new_path != entry_path.path.as_ref() {
1801                let task = project.rename_entry(entry_to_move, new_path, cx);
1802                cx.foreground_executor().spawn(task).detach_and_log_err(cx);
1803            }
1804
1805            project.worktree_id_for_entry(destination, cx)
1806        });
1807
1808        if let Some(destination_worktree) = destination_worktree {
1809            self.expand_entry(destination_worktree, destination, cx);
1810        }
1811    }
1812
1813    fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
1814        let mut entry_index = 0;
1815        let mut visible_entries_index = 0;
1816        for (worktree_index, (worktree_id, worktree_entries, _)) in
1817            self.visible_entries.iter().enumerate()
1818        {
1819            if *worktree_id == selection.worktree_id {
1820                for entry in worktree_entries {
1821                    if entry.id == selection.entry_id {
1822                        return Some((worktree_index, entry_index, visible_entries_index));
1823                    } else {
1824                        visible_entries_index += 1;
1825                        entry_index += 1;
1826                    }
1827                }
1828                break;
1829            } else {
1830                visible_entries_index += worktree_entries.len();
1831            }
1832        }
1833        None
1834    }
1835
1836    // Returns list of entries that should be affected by an operation.
1837    // When currently selected entry is not marked, it's treated as the only marked entry.
1838    fn marked_entries(&self) -> BTreeSet<SelectedEntry> {
1839        let Some(mut selection) = self.selection else {
1840            return Default::default();
1841        };
1842        if self.marked_entries.contains(&selection) {
1843            self.marked_entries
1844                .iter()
1845                .copied()
1846                .map(|mut entry| {
1847                    entry.entry_id = self.resolve_entry(entry.entry_id);
1848                    entry
1849                })
1850                .collect()
1851        } else {
1852            selection.entry_id = self.resolve_entry(selection.entry_id);
1853            BTreeSet::from_iter([selection])
1854        }
1855    }
1856
1857    /// Finds the currently selected subentry for a given leaf entry id. If a given entry
1858    /// has no ancestors, the project entry ID that's passed in is returned as-is.
1859    fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
1860        self.ancestors
1861            .get(&id)
1862            .and_then(|ancestors| {
1863                if ancestors.current_ancestor_depth == 0 {
1864                    return None;
1865                }
1866                ancestors.ancestors.get(ancestors.current_ancestor_depth)
1867            })
1868            .copied()
1869            .unwrap_or(id)
1870    }
1871
1872    pub fn selected_entry<'a>(
1873        &self,
1874        cx: &'a AppContext,
1875    ) -> Option<(&'a Worktree, &'a project::Entry)> {
1876        let (worktree, entry) = self.selected_entry_handle(cx)?;
1877        Some((worktree.read(cx), entry))
1878    }
1879
1880    /// Compared to selected_entry, this function resolves to the currently
1881    /// selected subentry if dir auto-folding is enabled.
1882    fn selected_sub_entry<'a>(
1883        &self,
1884        cx: &'a AppContext,
1885    ) -> Option<(Model<Worktree>, &'a project::Entry)> {
1886        let (worktree, mut entry) = self.selected_entry_handle(cx)?;
1887
1888        let resolved_id = self.resolve_entry(entry.id);
1889        if resolved_id != entry.id {
1890            let worktree = worktree.read(cx);
1891            entry = worktree.entry_for_id(resolved_id)?;
1892        }
1893        Some((worktree, entry))
1894    }
1895    fn selected_entry_handle<'a>(
1896        &self,
1897        cx: &'a AppContext,
1898    ) -> Option<(Model<Worktree>, &'a project::Entry)> {
1899        let selection = self.selection?;
1900        let project = self.project.read(cx);
1901        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
1902        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
1903        Some((worktree, entry))
1904    }
1905
1906    fn expand_to_selection(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
1907        let (worktree, entry) = self.selected_entry(cx)?;
1908        let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
1909
1910        for path in entry.path.ancestors() {
1911            let Some(entry) = worktree.entry_for_path(path) else {
1912                continue;
1913            };
1914            if entry.is_dir() {
1915                if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
1916                    expanded_dir_ids.insert(idx, entry.id);
1917                }
1918            }
1919        }
1920
1921        Some(())
1922    }
1923
1924    fn update_visible_entries(
1925        &mut self,
1926        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
1927        cx: &mut ViewContext<Self>,
1928    ) {
1929        let auto_collapse_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
1930        let project = self.project.read(cx);
1931        self.last_worktree_root_id = project
1932            .visible_worktrees(cx)
1933            .next_back()
1934            .and_then(|worktree| worktree.read(cx).root_entry())
1935            .map(|entry| entry.id);
1936
1937        let old_ancestors = std::mem::take(&mut self.ancestors);
1938        self.visible_entries.clear();
1939        let mut max_width_item = None;
1940        for worktree in project.visible_worktrees(cx) {
1941            let snapshot = worktree.read(cx).snapshot();
1942            let worktree_id = snapshot.id();
1943
1944            let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
1945                hash_map::Entry::Occupied(e) => e.into_mut(),
1946                hash_map::Entry::Vacant(e) => {
1947                    // The first time a worktree's root entry becomes available,
1948                    // mark that root entry as expanded.
1949                    if let Some(entry) = snapshot.root_entry() {
1950                        e.insert(vec![entry.id]).as_slice()
1951                    } else {
1952                        &[]
1953                    }
1954                }
1955            };
1956
1957            let mut new_entry_parent_id = None;
1958            let mut new_entry_kind = EntryKind::Dir;
1959            if let Some(edit_state) = &self.edit_state {
1960                if edit_state.worktree_id == worktree_id && edit_state.is_new_entry() {
1961                    new_entry_parent_id = Some(edit_state.entry_id);
1962                    new_entry_kind = if edit_state.is_dir {
1963                        EntryKind::Dir
1964                    } else {
1965                        EntryKind::File
1966                    };
1967                }
1968            }
1969
1970            let mut visible_worktree_entries = Vec::new();
1971            let mut entry_iter = snapshot.entries(true, 0);
1972            let mut auto_folded_ancestors = vec![];
1973            while let Some(entry) = entry_iter.entry() {
1974                if auto_collapse_dirs && entry.kind.is_dir() {
1975                    auto_folded_ancestors.push(entry.id);
1976                    if !self.unfolded_dir_ids.contains(&entry.id) {
1977                        if let Some(root_path) = snapshot.root_entry() {
1978                            let mut child_entries = snapshot.child_entries(&entry.path);
1979                            if let Some(child) = child_entries.next() {
1980                                if entry.path != root_path.path
1981                                    && child_entries.next().is_none()
1982                                    && child.kind.is_dir()
1983                                {
1984                                    entry_iter.advance();
1985
1986                                    continue;
1987                                }
1988                            }
1989                        }
1990                    }
1991                    let depth = old_ancestors
1992                        .get(&entry.id)
1993                        .map(|ancestor| ancestor.current_ancestor_depth)
1994                        .unwrap_or_default();
1995                    if let Some(edit_state) = &mut self.edit_state {
1996                        if edit_state.entry_id == entry.id {
1997                            edit_state.depth = depth;
1998                        }
1999                    }
2000                    let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
2001                    if ancestors.len() > 1 {
2002                        ancestors.reverse();
2003                        self.ancestors.insert(
2004                            entry.id,
2005                            FoldedAncestors {
2006                                current_ancestor_depth: depth,
2007                                ancestors,
2008                            },
2009                        );
2010                    }
2011                }
2012                auto_folded_ancestors.clear();
2013                visible_worktree_entries.push(entry.clone());
2014                let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
2015                    entry.id == new_entry_id || {
2016                        self.ancestors.get(&entry.id).map_or(false, |entries| {
2017                            entries
2018                                .ancestors
2019                                .iter()
2020                                .any(|entry_id| *entry_id == new_entry_id)
2021                        })
2022                    }
2023                } else {
2024                    false
2025                };
2026                if precedes_new_entry {
2027                    visible_worktree_entries.push(Entry {
2028                        id: NEW_ENTRY_ID,
2029                        kind: new_entry_kind,
2030                        path: entry.path.join("\0").into(),
2031                        inode: 0,
2032                        mtime: entry.mtime,
2033                        size: entry.size,
2034                        is_ignored: entry.is_ignored,
2035                        is_external: false,
2036                        is_private: false,
2037                        git_status: entry.git_status,
2038                        canonical_path: entry.canonical_path.clone(),
2039                        char_bag: entry.char_bag,
2040                        is_fifo: entry.is_fifo,
2041                    });
2042                }
2043                let worktree_abs_path = worktree.read(cx).abs_path();
2044                let (depth, path) = if Some(entry) == worktree.read(cx).root_entry() {
2045                    let Some(path_name) = worktree_abs_path
2046                        .file_name()
2047                        .with_context(|| {
2048                            format!("Worktree abs path has no file name, root entry: {entry:?}")
2049                        })
2050                        .log_err()
2051                    else {
2052                        continue;
2053                    };
2054                    let path = Arc::from(Path::new(path_name));
2055                    let depth = 0;
2056                    (depth, path)
2057                } else if entry.is_file() {
2058                    let Some(path_name) = entry
2059                        .path
2060                        .file_name()
2061                        .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
2062                        .log_err()
2063                    else {
2064                        continue;
2065                    };
2066                    let path = Arc::from(Path::new(path_name));
2067                    let depth = entry.path.ancestors().count() - 1;
2068                    (depth, path)
2069                } else {
2070                    let path = self
2071                        .ancestors
2072                        .get(&entry.id)
2073                        .and_then(|ancestors| {
2074                            let outermost_ancestor = ancestors.ancestors.last()?;
2075                            let root_folded_entry = worktree
2076                                .read(cx)
2077                                .entry_for_id(*outermost_ancestor)?
2078                                .path
2079                                .as_ref();
2080                            entry
2081                                .path
2082                                .strip_prefix(root_folded_entry)
2083                                .ok()
2084                                .and_then(|suffix| {
2085                                    let full_path = Path::new(root_folded_entry.file_name()?);
2086                                    Some(Arc::<Path>::from(full_path.join(suffix)))
2087                                })
2088                        })
2089                        .or_else(|| entry.path.file_name().map(Path::new).map(Arc::from))
2090                        .unwrap_or_else(|| entry.path.clone());
2091                    let depth = path.components().count();
2092                    (depth, path)
2093                };
2094                let width_estimate = item_width_estimate(
2095                    depth,
2096                    path.to_string_lossy().chars().count(),
2097                    entry.canonical_path.is_some(),
2098                );
2099
2100                match max_width_item.as_mut() {
2101                    Some((id, worktree_id, width)) => {
2102                        if *width < width_estimate {
2103                            *id = entry.id;
2104                            *worktree_id = worktree.read(cx).id();
2105                            *width = width_estimate;
2106                        }
2107                    }
2108                    None => {
2109                        max_width_item = Some((entry.id, worktree.read(cx).id(), width_estimate))
2110                    }
2111                }
2112
2113                if expanded_dir_ids.binary_search(&entry.id).is_err()
2114                    && entry_iter.advance_to_sibling()
2115                {
2116                    continue;
2117                }
2118                entry_iter.advance();
2119            }
2120
2121            snapshot.propagate_git_statuses(&mut visible_worktree_entries);
2122            project::sort_worktree_entries(&mut visible_worktree_entries);
2123            self.visible_entries
2124                .push((worktree_id, visible_worktree_entries, OnceCell::new()));
2125        }
2126
2127        if let Some((project_entry_id, worktree_id, _)) = max_width_item {
2128            let mut visited_worktrees_length = 0;
2129            let index = self.visible_entries.iter().find_map(|(id, entries, _)| {
2130                if worktree_id == *id {
2131                    entries
2132                        .iter()
2133                        .position(|entry| entry.id == project_entry_id)
2134                } else {
2135                    visited_worktrees_length += entries.len();
2136                    None
2137                }
2138            });
2139            if let Some(index) = index {
2140                self.max_width_item_index = Some(visited_worktrees_length + index);
2141            }
2142        }
2143        if let Some((worktree_id, entry_id)) = new_selected_entry {
2144            self.selection = Some(SelectedEntry {
2145                worktree_id,
2146                entry_id,
2147            });
2148        }
2149    }
2150
2151    fn expand_entry(
2152        &mut self,
2153        worktree_id: WorktreeId,
2154        entry_id: ProjectEntryId,
2155        cx: &mut ViewContext<Self>,
2156    ) {
2157        self.project.update(cx, |project, cx| {
2158            if let Some((worktree, expanded_dir_ids)) = project
2159                .worktree_for_id(worktree_id, cx)
2160                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
2161            {
2162                project.expand_entry(worktree_id, entry_id, cx);
2163                let worktree = worktree.read(cx);
2164
2165                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
2166                    loop {
2167                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
2168                            expanded_dir_ids.insert(ix, entry.id);
2169                        }
2170
2171                        if let Some(parent_entry) =
2172                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
2173                        {
2174                            entry = parent_entry;
2175                        } else {
2176                            break;
2177                        }
2178                    }
2179                }
2180            }
2181        });
2182    }
2183
2184    fn drop_external_files(
2185        &mut self,
2186        paths: &[PathBuf],
2187        entry_id: ProjectEntryId,
2188        cx: &mut ViewContext<Self>,
2189    ) {
2190        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
2191
2192        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
2193
2194        let Some((target_directory, worktree)) = maybe!({
2195            let worktree = self.project.read(cx).worktree_for_entry(entry_id, cx)?;
2196            let entry = worktree.read(cx).entry_for_id(entry_id)?;
2197            let path = worktree.read(cx).absolutize(&entry.path).ok()?;
2198            let target_directory = if path.is_dir() {
2199                path
2200            } else {
2201                path.parent()?.to_path_buf()
2202            };
2203            Some((target_directory, worktree))
2204        }) else {
2205            return;
2206        };
2207
2208        let mut paths_to_replace = Vec::new();
2209        for path in &paths {
2210            if let Some(name) = path.file_name() {
2211                let mut target_path = target_directory.clone();
2212                target_path.push(name);
2213                if target_path.exists() {
2214                    paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
2215                }
2216            }
2217        }
2218
2219        cx.spawn(|this, mut cx| {
2220            async move {
2221                for (filename, original_path) in &paths_to_replace {
2222                    let answer = cx
2223                        .prompt(
2224                            PromptLevel::Info,
2225                            format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(),
2226                            None,
2227                            &["Replace", "Cancel"],
2228                        )
2229                        .await?;
2230                    if answer == 1 {
2231                        if let Some(item_idx) = paths.iter().position(|p| p == original_path) {
2232                            paths.remove(item_idx);
2233                        }
2234                    }
2235                }
2236
2237                if paths.is_empty() {
2238                    return Ok(());
2239                }
2240
2241                let task = worktree.update(&mut cx, |worktree, cx| {
2242                    worktree.copy_external_entries(target_directory, paths, true, cx)
2243                })?;
2244
2245                let opened_entries = task.await?;
2246                this.update(&mut cx, |this, cx| {
2247                    if open_file_after_drop && !opened_entries.is_empty() {
2248                        this.open_entry(opened_entries[0], true, false, cx);
2249                    }
2250                })
2251            }
2252            .log_err()
2253        })
2254        .detach();
2255    }
2256
2257    fn drag_onto(
2258        &mut self,
2259        selections: &DraggedSelection,
2260        target_entry_id: ProjectEntryId,
2261        is_file: bool,
2262        cx: &mut ViewContext<Self>,
2263    ) {
2264        let should_copy = cx.modifiers().alt;
2265        if should_copy {
2266            let _ = maybe!({
2267                let project = self.project.read(cx);
2268                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
2269                let target_entry = target_worktree
2270                    .read(cx)
2271                    .entry_for_id(target_entry_id)?
2272                    .clone();
2273                for selection in selections.items() {
2274                    let new_path = self.create_paste_path(
2275                        selection,
2276                        (target_worktree.clone(), &target_entry),
2277                        cx,
2278                    )?;
2279                    self.project
2280                        .update(cx, |project, cx| {
2281                            project.copy_entry(selection.entry_id, None, new_path, cx)
2282                        })
2283                        .detach_and_log_err(cx)
2284                }
2285
2286                Some(())
2287            });
2288        } else {
2289            for selection in selections.items() {
2290                self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
2291            }
2292        }
2293    }
2294
2295    fn index_for_entry(
2296        &self,
2297        entry_id: ProjectEntryId,
2298        worktree_id: WorktreeId,
2299    ) -> Option<(usize, usize, usize)> {
2300        let mut worktree_ix = 0;
2301        let mut total_ix = 0;
2302        for (current_worktree_id, visible_worktree_entries, _) in &self.visible_entries {
2303            if worktree_id != *current_worktree_id {
2304                total_ix += visible_worktree_entries.len();
2305                worktree_ix += 1;
2306                continue;
2307            }
2308
2309            return visible_worktree_entries
2310                .iter()
2311                .enumerate()
2312                .find(|(_, entry)| entry.id == entry_id)
2313                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
2314        }
2315        None
2316    }
2317
2318    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, &Entry)> {
2319        let mut offset = 0;
2320        for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
2321            if visible_worktree_entries.len() > offset + index {
2322                return visible_worktree_entries
2323                    .get(index)
2324                    .map(|entry| (*worktree_id, entry));
2325            }
2326            offset += visible_worktree_entries.len();
2327        }
2328        None
2329    }
2330
2331    fn iter_visible_entries(
2332        &self,
2333        range: Range<usize>,
2334        cx: &mut ViewContext<ProjectPanel>,
2335        mut callback: impl FnMut(&Entry, &HashSet<Arc<Path>>, &mut ViewContext<ProjectPanel>),
2336    ) {
2337        let mut ix = 0;
2338        for (_, visible_worktree_entries, entries_paths) in &self.visible_entries {
2339            if ix >= range.end {
2340                return;
2341            }
2342
2343            if ix + visible_worktree_entries.len() <= range.start {
2344                ix += visible_worktree_entries.len();
2345                continue;
2346            }
2347
2348            let end_ix = range.end.min(ix + visible_worktree_entries.len());
2349            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
2350            let entries = entries_paths.get_or_init(|| {
2351                visible_worktree_entries
2352                    .iter()
2353                    .map(|e| (e.path.clone()))
2354                    .collect()
2355            });
2356            for entry in visible_worktree_entries[entry_range].iter() {
2357                callback(entry, entries, cx);
2358            }
2359            ix = end_ix;
2360        }
2361    }
2362
2363    fn for_each_visible_entry(
2364        &self,
2365        range: Range<usize>,
2366        cx: &mut ViewContext<ProjectPanel>,
2367        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
2368    ) {
2369        let mut ix = 0;
2370        for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
2371            if ix >= range.end {
2372                return;
2373            }
2374
2375            if ix + visible_worktree_entries.len() <= range.start {
2376                ix += visible_worktree_entries.len();
2377                continue;
2378            }
2379
2380            let end_ix = range.end.min(ix + visible_worktree_entries.len());
2381            let (git_status_setting, show_file_icons, show_folder_icons) = {
2382                let settings = ProjectPanelSettings::get_global(cx);
2383                (
2384                    settings.git_status,
2385                    settings.file_icons,
2386                    settings.folder_icons,
2387                )
2388            };
2389            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
2390                let snapshot = worktree.read(cx).snapshot();
2391                let root_name = OsStr::new(snapshot.root_name());
2392                let expanded_entry_ids = self
2393                    .expanded_dir_ids
2394                    .get(&snapshot.id())
2395                    .map(Vec::as_slice)
2396                    .unwrap_or(&[]);
2397
2398                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
2399                let entries = entries_paths.get_or_init(|| {
2400                    visible_worktree_entries
2401                        .iter()
2402                        .map(|e| (e.path.clone()))
2403                        .collect()
2404                });
2405                for entry in visible_worktree_entries[entry_range].iter() {
2406                    let status = git_status_setting.then_some(entry.git_status).flatten();
2407                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
2408                    let icon = match entry.kind {
2409                        EntryKind::File => {
2410                            if show_file_icons {
2411                                FileIcons::get_icon(&entry.path, cx)
2412                            } else {
2413                                None
2414                            }
2415                        }
2416                        _ => {
2417                            if show_folder_icons {
2418                                FileIcons::get_folder_icon(is_expanded, cx)
2419                            } else {
2420                                FileIcons::get_chevron_icon(is_expanded, cx)
2421                            }
2422                        }
2423                    };
2424
2425                    let (depth, difference) =
2426                        ProjectPanel::calculate_depth_and_difference(entry, entries);
2427
2428                    let filename = match difference {
2429                        diff if diff > 1 => entry
2430                            .path
2431                            .iter()
2432                            .skip(entry.path.components().count() - diff)
2433                            .collect::<PathBuf>()
2434                            .to_str()
2435                            .unwrap_or_default()
2436                            .to_string(),
2437                        _ => entry
2438                            .path
2439                            .file_name()
2440                            .map(|name| name.to_string_lossy().into_owned())
2441                            .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
2442                    };
2443                    let selection = SelectedEntry {
2444                        worktree_id: snapshot.id(),
2445                        entry_id: entry.id,
2446                    };
2447
2448                    let is_marked = self.marked_entries.contains(&selection);
2449
2450                    let diagnostic_severity = self
2451                        .diagnostics
2452                        .get(&(*worktree_id, entry.path.to_path_buf()))
2453                        .cloned();
2454
2455                    let filename_text_color =
2456                        entry_git_aware_label_color(status, entry.is_ignored, is_marked);
2457
2458                    let mut details = EntryDetails {
2459                        filename,
2460                        icon,
2461                        path: entry.path.clone(),
2462                        depth,
2463                        kind: entry.kind,
2464                        is_ignored: entry.is_ignored,
2465                        is_expanded,
2466                        is_selected: self.selection == Some(selection),
2467                        is_marked,
2468                        is_editing: false,
2469                        is_processing: false,
2470                        is_cut: self
2471                            .clipboard
2472                            .as_ref()
2473                            .map_or(false, |e| e.is_cut() && e.items().contains(&selection)),
2474                        filename_text_color,
2475                        diagnostic_severity,
2476                        git_status: status,
2477                        is_private: entry.is_private,
2478                        worktree_id: *worktree_id,
2479                        canonical_path: entry.canonical_path.clone(),
2480                    };
2481
2482                    if let Some(edit_state) = &self.edit_state {
2483                        let is_edited_entry = if edit_state.is_new_entry() {
2484                            entry.id == NEW_ENTRY_ID
2485                        } else {
2486                            entry.id == edit_state.entry_id
2487                                || self
2488                                    .ancestors
2489                                    .get(&entry.id)
2490                                    .is_some_and(|auto_folded_dirs| {
2491                                        auto_folded_dirs
2492                                            .ancestors
2493                                            .iter()
2494                                            .any(|entry_id| *entry_id == edit_state.entry_id)
2495                                    })
2496                        };
2497
2498                        if is_edited_entry {
2499                            if let Some(processing_filename) = &edit_state.processing_filename {
2500                                details.is_processing = true;
2501                                if let Some(ancestors) = edit_state
2502                                    .leaf_entry_id
2503                                    .and_then(|entry| self.ancestors.get(&entry))
2504                                {
2505                                    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;
2506                                    let all_components = ancestors.ancestors.len();
2507
2508                                    let prefix_components = all_components - position;
2509                                    let suffix_components = position.checked_sub(1);
2510                                    let mut previous_components =
2511                                        Path::new(&details.filename).components();
2512                                    let mut new_path = previous_components
2513                                        .by_ref()
2514                                        .take(prefix_components)
2515                                        .collect::<PathBuf>();
2516                                    if let Some(last_component) =
2517                                        Path::new(processing_filename).components().last()
2518                                    {
2519                                        new_path.push(last_component);
2520                                        previous_components.next();
2521                                    }
2522
2523                                    if let Some(_) = suffix_components {
2524                                        new_path.push(previous_components);
2525                                    }
2526                                    if let Some(str) = new_path.to_str() {
2527                                        details.filename.clear();
2528                                        details.filename.push_str(str);
2529                                    }
2530                                } else {
2531                                    details.filename.clear();
2532                                    details.filename.push_str(processing_filename);
2533                                }
2534                            } else {
2535                                if edit_state.is_new_entry() {
2536                                    details.filename.clear();
2537                                }
2538                                details.is_editing = true;
2539                            }
2540                        }
2541                    }
2542
2543                    callback(entry.id, details, cx);
2544                }
2545            }
2546            ix = end_ix;
2547        }
2548    }
2549
2550    fn calculate_depth_and_difference(
2551        entry: &Entry,
2552        visible_worktree_entries: &HashSet<Arc<Path>>,
2553    ) -> (usize, usize) {
2554        let (depth, difference) = entry
2555            .path
2556            .ancestors()
2557            .skip(1) // Skip the entry itself
2558            .find_map(|ancestor| {
2559                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
2560                    let entry_path_components_count = entry.path.components().count();
2561                    let parent_path_components_count = parent_entry.components().count();
2562                    let difference = entry_path_components_count - parent_path_components_count;
2563                    let depth = parent_entry
2564                        .ancestors()
2565                        .skip(1)
2566                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
2567                        .count();
2568                    Some((depth + 1, difference))
2569                } else {
2570                    None
2571                }
2572            })
2573            .unwrap_or((0, 0));
2574
2575        (depth, difference)
2576    }
2577
2578    fn render_entry(
2579        &self,
2580        entry_id: ProjectEntryId,
2581        details: EntryDetails,
2582        cx: &mut ViewContext<Self>,
2583    ) -> Stateful<Div> {
2584        let kind = details.kind;
2585        let settings = ProjectPanelSettings::get_global(cx);
2586        let show_editor = details.is_editing && !details.is_processing;
2587
2588        let selection = SelectedEntry {
2589            worktree_id: details.worktree_id,
2590            entry_id,
2591        };
2592
2593        let is_marked = self.marked_entries.contains(&selection);
2594        let is_active = self
2595            .selection
2596            .map_or(false, |selection| selection.entry_id == entry_id);
2597
2598        let width = self.size(cx);
2599        let file_name = details.filename.clone();
2600
2601        let mut icon = details.icon.clone();
2602        if settings.file_icons && show_editor && details.kind.is_file() {
2603            let filename = self.filename_editor.read(cx).text(cx);
2604            if filename.len() > 2 {
2605                icon = FileIcons::get_icon(Path::new(&filename), cx);
2606            }
2607        }
2608
2609        let filename_text_color = details.filename_text_color;
2610        let diagnostic_severity = details.diagnostic_severity;
2611        let item_colors = get_item_color(cx);
2612
2613        let canonical_path = details
2614            .canonical_path
2615            .as_ref()
2616            .map(|f| f.to_string_lossy().to_string());
2617        let path = details.path.clone();
2618
2619        let depth = details.depth;
2620        let worktree_id = details.worktree_id;
2621        let selections = Arc::new(self.marked_entries.clone());
2622        let is_local = self.project.read(cx).is_local();
2623
2624        let dragged_selection = DraggedSelection {
2625            active_selection: selection,
2626            marked_selections: selections,
2627        };
2628        div()
2629            .id(entry_id.to_proto() as usize)
2630            .when(is_local, |div| {
2631                div.on_drag_move::<ExternalPaths>(cx.listener(
2632                    move |this, event: &DragMoveEvent<ExternalPaths>, cx| {
2633                        if event.bounds.contains(&event.event.position) {
2634                            if this.last_external_paths_drag_over_entry == Some(entry_id) {
2635                                return;
2636                            }
2637                            this.last_external_paths_drag_over_entry = Some(entry_id);
2638                            this.marked_entries.clear();
2639
2640                            let Some((worktree, path, entry)) = maybe!({
2641                                let worktree = this
2642                                    .project
2643                                    .read(cx)
2644                                    .worktree_for_id(selection.worktree_id, cx)?;
2645                                let worktree = worktree.read(cx);
2646                                let abs_path = worktree.absolutize(&path).log_err()?;
2647                                let path = if abs_path.is_dir() {
2648                                    path.as_ref()
2649                                } else {
2650                                    path.parent()?
2651                                };
2652                                let entry = worktree.entry_for_path(path)?;
2653                                Some((worktree, path, entry))
2654                            }) else {
2655                                return;
2656                            };
2657
2658                            this.marked_entries.insert(SelectedEntry {
2659                                entry_id: entry.id,
2660                                worktree_id: worktree.id(),
2661                            });
2662
2663                            for entry in worktree.child_entries(path) {
2664                                this.marked_entries.insert(SelectedEntry {
2665                                    entry_id: entry.id,
2666                                    worktree_id: worktree.id(),
2667                                });
2668                            }
2669
2670                            cx.notify();
2671                        }
2672                    },
2673                ))
2674                .on_drop(cx.listener(
2675                    move |this, external_paths: &ExternalPaths, cx| {
2676                        this.hover_scroll_task.take();
2677                        this.last_external_paths_drag_over_entry = None;
2678                        this.marked_entries.clear();
2679                        this.drop_external_files(external_paths.paths(), entry_id, cx);
2680                        cx.stop_propagation();
2681                    },
2682                ))
2683            })
2684            .on_drag(dragged_selection, move |selection, cx| {
2685                cx.new_view(|_| DraggedProjectEntryView {
2686                    details: details.clone(),
2687                    width,
2688                    selection: selection.active_selection,
2689                    selections: selection.marked_selections.clone(),
2690                })
2691            })
2692            .drag_over::<DraggedSelection>(move |style, _, _| style.bg(item_colors.drag_over))
2693            .on_drop(cx.listener(move |this, selections: &DraggedSelection, cx| {
2694                this.hover_scroll_task.take();
2695                this.drag_onto(selections, entry_id, kind.is_file(), cx);
2696            }))
2697            .on_mouse_down(
2698                MouseButton::Left,
2699                cx.listener(move |this, _, cx| {
2700                    this.mouse_down = true;
2701                    cx.propagate();
2702                }),
2703            )
2704            .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
2705                if event.down.button == MouseButton::Right || event.down.first_mouse || show_editor
2706                {
2707                    return;
2708                }
2709                if event.down.button == MouseButton::Left {
2710                    this.mouse_down = false;
2711                }
2712                cx.stop_propagation();
2713
2714                if let Some(selection) = this.selection.filter(|_| event.down.modifiers.shift) {
2715                    let current_selection = this.index_for_selection(selection);
2716                    let clicked_entry = SelectedEntry {
2717                        entry_id,
2718                        worktree_id,
2719                    };
2720                    let target_selection = this.index_for_selection(clicked_entry);
2721                    if let Some(((_, _, source_index), (_, _, target_index))) =
2722                        current_selection.zip(target_selection)
2723                    {
2724                        let range_start = source_index.min(target_index);
2725                        let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
2726                        let mut new_selections = BTreeSet::new();
2727                        this.for_each_visible_entry(
2728                            range_start..range_end,
2729                            cx,
2730                            |entry_id, details, _| {
2731                                new_selections.insert(SelectedEntry {
2732                                    entry_id,
2733                                    worktree_id: details.worktree_id,
2734                                });
2735                            },
2736                        );
2737
2738                        this.marked_entries = this
2739                            .marked_entries
2740                            .union(&new_selections)
2741                            .cloned()
2742                            .collect();
2743
2744                        this.selection = Some(clicked_entry);
2745                        this.marked_entries.insert(clicked_entry);
2746                    }
2747                } else if event.down.modifiers.secondary() {
2748                    if event.down.click_count > 1 {
2749                        this.split_entry(entry_id, cx);
2750                    } else if !this.marked_entries.insert(selection) {
2751                        this.marked_entries.remove(&selection);
2752                    }
2753                } else if kind.is_dir() {
2754                    this.toggle_expanded(entry_id, cx);
2755                } else {
2756                    let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
2757                    let click_count = event.up.click_count;
2758                    let focus_opened_item = !preview_tabs_enabled || click_count > 1;
2759                    let allow_preview = preview_tabs_enabled && click_count == 1;
2760                    this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
2761                }
2762            }))
2763            .cursor_pointer()
2764            .child(
2765                ListItem::new(entry_id.to_proto() as usize)
2766                    .indent_level(depth)
2767                    .indent_step_size(px(settings.indent_size))
2768                    .selected(is_marked || is_active)
2769                    .when_some(canonical_path, |this, path| {
2770                        this.end_slot::<AnyElement>(
2771                            div()
2772                                .id("symlink_icon")
2773                                .pr_3()
2774                                .tooltip(move |cx| {
2775                                    Tooltip::with_meta(path.to_string(), None, "Symbolic Link", cx)
2776                                })
2777                                .child(
2778                                    Icon::new(IconName::ArrowUpRight)
2779                                        .size(IconSize::Indicator)
2780                                        .color(filename_text_color),
2781                                )
2782                                .into_any_element(),
2783                        )
2784                    })
2785                    .child(if let Some(icon) = &icon {
2786                        // Check if there's a diagnostic severity and get the decoration color
2787                        if let Some((_, decoration_color)) =
2788                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
2789                        {
2790                            // Determine if the diagnostic is a warning
2791                            let is_warning = diagnostic_severity
2792                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
2793                                .unwrap_or(false);
2794                            div().child(
2795                                DecoratedIcon::new(
2796                                    Icon::from_path(icon.clone()).color(Color::Muted),
2797                                    Some(
2798                                        IconDecoration::new(
2799                                            if kind.is_file() {
2800                                                if is_warning {
2801                                                    IconDecorationKind::Triangle
2802                                                } else {
2803                                                    IconDecorationKind::X
2804                                                }
2805                                            } else {
2806                                                IconDecorationKind::Dot
2807                                            },
2808                                            if is_marked || is_active {
2809                                                item_colors.selected
2810                                            } else {
2811                                                item_colors.default
2812                                            },
2813                                            cx,
2814                                        )
2815                                        .color(decoration_color.color(cx))
2816                                        .position(Point {
2817                                            x: px(-2.),
2818                                            y: px(-2.),
2819                                        }),
2820                                    ),
2821                                )
2822                                .into_any_element(),
2823                            )
2824                        } else {
2825                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
2826                        }
2827                    } else {
2828                        if let Some((icon_name, color)) =
2829                            entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
2830                        {
2831                            h_flex()
2832                                .size(IconSize::default().rems())
2833                                .child(Icon::new(icon_name).color(color).size(IconSize::Small))
2834                        } else {
2835                            h_flex()
2836                                .size(IconSize::default().rems())
2837                                .invisible()
2838                                .flex_none()
2839                        }
2840                    })
2841                    .child(
2842                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
2843                            h_flex().h_6().w_full().child(editor.clone())
2844                        } else {
2845                            h_flex().h_6().map(|mut this| {
2846                                if let Some(folded_ancestors) = self.ancestors.get(&entry_id) {
2847                                    let components = Path::new(&file_name)
2848                                        .components()
2849                                        .map(|comp| {
2850                                            let comp_str =
2851                                                comp.as_os_str().to_string_lossy().into_owned();
2852                                            comp_str
2853                                        })
2854                                        .collect::<Vec<_>>();
2855
2856                                    let components_len = components.len();
2857                                    let active_index = components_len
2858                                        - 1
2859                                        - folded_ancestors.current_ancestor_depth;
2860                                    const DELIMITER: SharedString =
2861                                        SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
2862                                    for (index, component) in components.into_iter().enumerate() {
2863                                        if index != 0 {
2864                                            this = this.child(
2865                                                Label::new(DELIMITER.clone())
2866                                                    .single_line()
2867                                                    .color(filename_text_color),
2868                                            );
2869                                        }
2870                                        let id = SharedString::from(format!(
2871                                            "project_panel_path_component_{}_{index}",
2872                                            entry_id.to_usize()
2873                                        ));
2874                                        let label = div()
2875                                            .id(id)
2876                                            .on_click(cx.listener(move |this, _, cx| {
2877                                                if index != active_index {
2878                                                    if let Some(folds) =
2879                                                        this.ancestors.get_mut(&entry_id)
2880                                                    {
2881                                                        folds.current_ancestor_depth =
2882                                                            components_len - 1 - index;
2883                                                        cx.notify();
2884                                                    }
2885                                                }
2886                                            }))
2887                                            .child(
2888                                                Label::new(component)
2889                                                    .single_line()
2890                                                    .color(filename_text_color)
2891                                                    .when(
2892                                                        is_active && index == active_index,
2893                                                        |this| this.underline(true),
2894                                                    ),
2895                                            );
2896
2897                                        this = this.child(label);
2898                                    }
2899
2900                                    this
2901                                } else {
2902                                    this.child(
2903                                        Label::new(file_name)
2904                                            .single_line()
2905                                            .color(filename_text_color),
2906                                    )
2907                                }
2908                            })
2909                        }
2910                        .ml_1(),
2911                    )
2912                    .on_secondary_mouse_down(cx.listener(
2913                        move |this, event: &MouseDownEvent, cx| {
2914                            // Stop propagation to prevent the catch-all context menu for the project
2915                            // panel from being deployed.
2916                            cx.stop_propagation();
2917                            this.deploy_context_menu(event.position, entry_id, cx);
2918                        },
2919                    ))
2920                    .overflow_x(),
2921            )
2922            .border_1()
2923            .border_r_2()
2924            .rounded_none()
2925            .hover(|style| {
2926                if is_active {
2927                    style
2928                } else {
2929                    style.bg(item_colors.hover).border_color(item_colors.hover)
2930                }
2931            })
2932            .when(is_marked || is_active, |this| {
2933                this.when(is_marked, |this| {
2934                    this.bg(item_colors.marked_active)
2935                        .border_color(item_colors.marked_active)
2936                })
2937            })
2938            .when(
2939                !self.mouse_down && is_active && self.focus_handle.contains_focused(cx),
2940                |this| this.border_color(Color::Selected.color(cx)),
2941            )
2942    }
2943
2944    fn render_vertical_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
2945        if !Self::should_show_scrollbar(cx)
2946            || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
2947        {
2948            return None;
2949        }
2950        Some(
2951            div()
2952                .occlude()
2953                .id("project-panel-vertical-scroll")
2954                .on_mouse_move(cx.listener(|_, _, cx| {
2955                    cx.notify();
2956                    cx.stop_propagation()
2957                }))
2958                .on_hover(|_, cx| {
2959                    cx.stop_propagation();
2960                })
2961                .on_any_mouse_down(|_, cx| {
2962                    cx.stop_propagation();
2963                })
2964                .on_mouse_up(
2965                    MouseButton::Left,
2966                    cx.listener(|this, _, cx| {
2967                        if !this.vertical_scrollbar_state.is_dragging()
2968                            && !this.focus_handle.contains_focused(cx)
2969                        {
2970                            this.hide_scrollbar(cx);
2971                            cx.notify();
2972                        }
2973
2974                        cx.stop_propagation();
2975                    }),
2976                )
2977                .on_scroll_wheel(cx.listener(|_, _, cx| {
2978                    cx.notify();
2979                }))
2980                .h_full()
2981                .absolute()
2982                .right_1()
2983                .top_1()
2984                .bottom_1()
2985                .w(px(12.))
2986                .cursor_default()
2987                .children(Scrollbar::vertical(
2988                    // percentage as f32..end_offset as f32,
2989                    self.vertical_scrollbar_state.clone(),
2990                )),
2991        )
2992    }
2993
2994    fn render_horizontal_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
2995        if !Self::should_show_scrollbar(cx)
2996            || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
2997        {
2998            return None;
2999        }
3000
3001        let scroll_handle = self.scroll_handle.0.borrow();
3002        let longest_item_width = scroll_handle
3003            .last_item_size
3004            .filter(|size| size.contents.width > size.item.width)?
3005            .contents
3006            .width
3007            .0 as f64;
3008        if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
3009            return None;
3010        }
3011
3012        Some(
3013            div()
3014                .occlude()
3015                .id("project-panel-horizontal-scroll")
3016                .on_mouse_move(cx.listener(|_, _, cx| {
3017                    cx.notify();
3018                    cx.stop_propagation()
3019                }))
3020                .on_hover(|_, cx| {
3021                    cx.stop_propagation();
3022                })
3023                .on_any_mouse_down(|_, cx| {
3024                    cx.stop_propagation();
3025                })
3026                .on_mouse_up(
3027                    MouseButton::Left,
3028                    cx.listener(|this, _, cx| {
3029                        if !this.horizontal_scrollbar_state.is_dragging()
3030                            && !this.focus_handle.contains_focused(cx)
3031                        {
3032                            this.hide_scrollbar(cx);
3033                            cx.notify();
3034                        }
3035
3036                        cx.stop_propagation();
3037                    }),
3038                )
3039                .on_scroll_wheel(cx.listener(|_, _, cx| {
3040                    cx.notify();
3041                }))
3042                .w_full()
3043                .absolute()
3044                .right_1()
3045                .left_1()
3046                .bottom_1()
3047                .h(px(12.))
3048                .cursor_default()
3049                .when(self.width.is_some(), |this| {
3050                    this.children(Scrollbar::horizontal(
3051                        self.horizontal_scrollbar_state.clone(),
3052                    ))
3053                }),
3054        )
3055    }
3056
3057    fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
3058        let mut dispatch_context = KeyContext::new_with_defaults();
3059        dispatch_context.add("ProjectPanel");
3060        dispatch_context.add("menu");
3061
3062        let identifier = if self.filename_editor.focus_handle(cx).is_focused(cx) {
3063            "editing"
3064        } else {
3065            "not_editing"
3066        };
3067
3068        dispatch_context.add(identifier);
3069        dispatch_context
3070    }
3071
3072    fn should_show_scrollbar(cx: &AppContext) -> bool {
3073        let show = ProjectPanelSettings::get_global(cx)
3074            .scrollbar
3075            .show
3076            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
3077        match show {
3078            ShowScrollbar::Auto => true,
3079            ShowScrollbar::System => true,
3080            ShowScrollbar::Always => true,
3081            ShowScrollbar::Never => false,
3082        }
3083    }
3084
3085    fn should_autohide_scrollbar(cx: &AppContext) -> bool {
3086        let show = ProjectPanelSettings::get_global(cx)
3087            .scrollbar
3088            .show
3089            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
3090        match show {
3091            ShowScrollbar::Auto => true,
3092            ShowScrollbar::System => cx
3093                .try_global::<ScrollbarAutoHide>()
3094                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
3095            ShowScrollbar::Always => false,
3096            ShowScrollbar::Never => true,
3097        }
3098    }
3099
3100    fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
3101        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
3102        if !Self::should_autohide_scrollbar(cx) {
3103            return;
3104        }
3105        self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
3106            cx.background_executor()
3107                .timer(SCROLLBAR_SHOW_INTERVAL)
3108                .await;
3109            panel
3110                .update(&mut cx, |panel, cx| {
3111                    panel.show_scrollbar = false;
3112                    cx.notify();
3113                })
3114                .log_err();
3115        }))
3116    }
3117
3118    fn reveal_entry(
3119        &mut self,
3120        project: Model<Project>,
3121        entry_id: ProjectEntryId,
3122        skip_ignored: bool,
3123        cx: &mut ViewContext<'_, Self>,
3124    ) {
3125        if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
3126            let worktree = worktree.read(cx);
3127            if skip_ignored
3128                && worktree
3129                    .entry_for_id(entry_id)
3130                    .map_or(true, |entry| entry.is_ignored)
3131            {
3132                return;
3133            }
3134
3135            let worktree_id = worktree.id();
3136            self.expand_entry(worktree_id, entry_id, cx);
3137            self.update_visible_entries(Some((worktree_id, entry_id)), cx);
3138
3139            if self.marked_entries.len() == 1
3140                && self
3141                    .marked_entries
3142                    .first()
3143                    .filter(|entry| entry.entry_id == entry_id)
3144                    .is_none()
3145            {
3146                self.marked_entries.clear();
3147            }
3148            self.autoscroll(cx);
3149            cx.notify();
3150        }
3151    }
3152
3153    fn find_active_indent_guide(
3154        &self,
3155        indent_guides: &[IndentGuideLayout],
3156        cx: &AppContext,
3157    ) -> Option<usize> {
3158        let (worktree, entry) = self.selected_entry(cx)?;
3159
3160        // Find the parent entry of the indent guide, this will either be the
3161        // expanded folder we have selected, or the parent of the currently
3162        // selected file/collapsed directory
3163        let mut entry = entry;
3164        loop {
3165            let is_expanded_dir = entry.is_dir()
3166                && self
3167                    .expanded_dir_ids
3168                    .get(&worktree.id())
3169                    .map(|ids| ids.binary_search(&entry.id).is_ok())
3170                    .unwrap_or(false);
3171            if is_expanded_dir {
3172                break;
3173            }
3174            entry = worktree.entry_for_path(&entry.path.parent()?)?;
3175        }
3176
3177        let (active_indent_range, depth) = {
3178            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
3179            let child_paths = &self.visible_entries[worktree_ix].1;
3180            let mut child_count = 0;
3181            let depth = entry.path.ancestors().count();
3182            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
3183                if entry.path.ancestors().count() <= depth {
3184                    break;
3185                }
3186                child_count += 1;
3187            }
3188
3189            let start = ix + 1;
3190            let end = start + child_count;
3191
3192            let (_, entries, paths) = &self.visible_entries[worktree_ix];
3193            let visible_worktree_entries =
3194                paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect());
3195
3196            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
3197            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
3198            (start..end, depth)
3199        };
3200
3201        let candidates = indent_guides
3202            .iter()
3203            .enumerate()
3204            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
3205
3206        for (i, indent) in candidates {
3207            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
3208            if active_indent_range.start <= indent.offset.y + indent.length
3209                && indent.offset.y <= active_indent_range.end
3210            {
3211                return Some(i);
3212            }
3213        }
3214        None
3215    }
3216}
3217
3218fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
3219    const ICON_SIZE_FACTOR: usize = 2;
3220    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
3221    if is_symlink {
3222        item_width += ICON_SIZE_FACTOR;
3223    }
3224    item_width
3225}
3226
3227impl Render for ProjectPanel {
3228    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
3229        let has_worktree = !self.visible_entries.is_empty();
3230        let project = self.project.read(cx);
3231        let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
3232        let show_indent_guides =
3233            ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
3234        let is_local = project.is_local();
3235
3236        if has_worktree {
3237            let item_count = self
3238                .visible_entries
3239                .iter()
3240                .map(|(_, worktree_entries, _)| worktree_entries.len())
3241                .sum();
3242
3243            fn handle_drag_move_scroll<T: 'static>(
3244                this: &mut ProjectPanel,
3245                e: &DragMoveEvent<T>,
3246                cx: &mut ViewContext<ProjectPanel>,
3247            ) {
3248                if !e.bounds.contains(&e.event.position) {
3249                    return;
3250                }
3251                this.hover_scroll_task.take();
3252                let panel_height = e.bounds.size.height;
3253                if panel_height <= px(0.) {
3254                    return;
3255                }
3256
3257                let event_offset = e.event.position.y - e.bounds.origin.y;
3258                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
3259                let hovered_region_offset = event_offset / panel_height;
3260
3261                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
3262                // These pixels offsets were picked arbitrarily.
3263                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
3264                    8.
3265                } else if hovered_region_offset <= 0.15 {
3266                    5.
3267                } else if hovered_region_offset >= 0.95 {
3268                    -8.
3269                } else if hovered_region_offset >= 0.85 {
3270                    -5.
3271                } else {
3272                    return;
3273                };
3274                let adjustment = point(px(0.), px(vertical_scroll_offset));
3275                this.hover_scroll_task = Some(cx.spawn(move |this, mut cx| async move {
3276                    loop {
3277                        let should_stop_scrolling = this
3278                            .update(&mut cx, |this, cx| {
3279                                this.hover_scroll_task.as_ref()?;
3280                                let handle = this.scroll_handle.0.borrow_mut();
3281                                let offset = handle.base_handle.offset();
3282
3283                                handle.base_handle.set_offset(offset + adjustment);
3284                                cx.notify();
3285                                Some(())
3286                            })
3287                            .ok()
3288                            .flatten()
3289                            .is_some();
3290                        if should_stop_scrolling {
3291                            return;
3292                        }
3293                        cx.background_executor()
3294                            .timer(Duration::from_millis(16))
3295                            .await;
3296                    }
3297                }));
3298            }
3299            h_flex()
3300                .id("project-panel")
3301                .group("project-panel")
3302                .on_drag_move(cx.listener(handle_drag_move_scroll::<ExternalPaths>))
3303                .on_drag_move(cx.listener(handle_drag_move_scroll::<DraggedSelection>))
3304                .size_full()
3305                .relative()
3306                .on_hover(cx.listener(|this, hovered, cx| {
3307                    if *hovered {
3308                        this.show_scrollbar = true;
3309                        this.hide_scrollbar_task.take();
3310                        cx.notify();
3311                    } else if !this.focus_handle.contains_focused(cx) {
3312                        this.hide_scrollbar(cx);
3313                    }
3314                }))
3315                .key_context(self.dispatch_context(cx))
3316                .on_action(cx.listener(Self::select_next))
3317                .on_action(cx.listener(Self::select_prev))
3318                .on_action(cx.listener(Self::select_first))
3319                .on_action(cx.listener(Self::select_last))
3320                .on_action(cx.listener(Self::select_parent))
3321                .on_action(cx.listener(Self::expand_selected_entry))
3322                .on_action(cx.listener(Self::collapse_selected_entry))
3323                .on_action(cx.listener(Self::collapse_all_entries))
3324                .on_action(cx.listener(Self::open))
3325                .on_action(cx.listener(Self::open_permanent))
3326                .on_action(cx.listener(Self::confirm))
3327                .on_action(cx.listener(Self::cancel))
3328                .on_action(cx.listener(Self::copy_path))
3329                .on_action(cx.listener(Self::copy_relative_path))
3330                .on_action(cx.listener(Self::new_search_in_directory))
3331                .on_action(cx.listener(Self::unfold_directory))
3332                .on_action(cx.listener(Self::fold_directory))
3333                .on_action(cx.listener(Self::remove_from_project))
3334                .when(!project.is_read_only(cx), |el| {
3335                    el.on_action(cx.listener(Self::new_file))
3336                        .on_action(cx.listener(Self::new_directory))
3337                        .on_action(cx.listener(Self::rename))
3338                        .on_action(cx.listener(Self::delete))
3339                        .on_action(cx.listener(Self::trash))
3340                        .on_action(cx.listener(Self::cut))
3341                        .on_action(cx.listener(Self::copy))
3342                        .on_action(cx.listener(Self::paste))
3343                        .on_action(cx.listener(Self::duplicate))
3344                        .on_click(cx.listener(|this, event: &gpui::ClickEvent, cx| {
3345                            if event.up.click_count > 1 {
3346                                if let Some(entry_id) = this.last_worktree_root_id {
3347                                    let project = this.project.read(cx);
3348
3349                                    let worktree_id = if let Some(worktree) =
3350                                        project.worktree_for_entry(entry_id, cx)
3351                                    {
3352                                        worktree.read(cx).id()
3353                                    } else {
3354                                        return;
3355                                    };
3356
3357                                    this.selection = Some(SelectedEntry {
3358                                        worktree_id,
3359                                        entry_id,
3360                                    });
3361
3362                                    this.new_file(&NewFile, cx);
3363                                }
3364                            }
3365                        }))
3366                })
3367                .when(project.is_local(), |el| {
3368                    el.on_action(cx.listener(Self::reveal_in_finder))
3369                        .on_action(cx.listener(Self::open_system))
3370                        .on_action(cx.listener(Self::open_in_terminal))
3371                })
3372                .when(project.is_via_ssh(), |el| {
3373                    el.on_action(cx.listener(Self::open_in_terminal))
3374                })
3375                .on_mouse_down(
3376                    MouseButton::Right,
3377                    cx.listener(move |this, event: &MouseDownEvent, cx| {
3378                        // When deploying the context menu anywhere below the last project entry,
3379                        // act as if the user clicked the root of the last worktree.
3380                        if let Some(entry_id) = this.last_worktree_root_id {
3381                            this.deploy_context_menu(event.position, entry_id, cx);
3382                        }
3383                    }),
3384                )
3385                .track_focus(&self.focus_handle(cx))
3386                .child(
3387                    uniform_list(cx.view().clone(), "entries", item_count, {
3388                        |this, range, cx| {
3389                            let mut items = Vec::with_capacity(range.end - range.start);
3390                            this.for_each_visible_entry(range, cx, |id, details, cx| {
3391                                items.push(this.render_entry(id, details, cx));
3392                            });
3393                            items
3394                        }
3395                    })
3396                    .when(show_indent_guides, |list| {
3397                        list.with_decoration(
3398                            ui::indent_guides(
3399                                cx.view().clone(),
3400                                px(indent_size),
3401                                IndentGuideColors::panel(cx),
3402                                |this, range, cx| {
3403                                    let mut items =
3404                                        SmallVec::with_capacity(range.end - range.start);
3405                                    this.iter_visible_entries(range, cx, |entry, entries, _| {
3406                                        let (depth, _) =
3407                                            Self::calculate_depth_and_difference(entry, entries);
3408                                        items.push(depth);
3409                                    });
3410                                    items
3411                                },
3412                            )
3413                            .on_click(cx.listener(
3414                                |this, active_indent_guide: &IndentGuideLayout, cx| {
3415                                    if cx.modifiers().secondary() {
3416                                        let ix = active_indent_guide.offset.y;
3417                                        let Some((target_entry, worktree)) = maybe!({
3418                                            let (worktree_id, entry) = this.entry_at_index(ix)?;
3419                                            let worktree = this
3420                                                .project
3421                                                .read(cx)
3422                                                .worktree_for_id(worktree_id, cx)?;
3423                                            let target_entry = worktree
3424                                                .read(cx)
3425                                                .entry_for_path(&entry.path.parent()?)?;
3426                                            Some((target_entry, worktree))
3427                                        }) else {
3428                                            return;
3429                                        };
3430
3431                                        this.collapse_entry(target_entry.clone(), worktree, cx);
3432                                    }
3433                                },
3434                            ))
3435                            .with_render_fn(
3436                                cx.view().clone(),
3437                                move |this, params, cx| {
3438                                    const LEFT_OFFSET: f32 = 14.;
3439                                    const PADDING_Y: f32 = 4.;
3440                                    const HITBOX_OVERDRAW: f32 = 3.;
3441
3442                                    let active_indent_guide_index =
3443                                        this.find_active_indent_guide(&params.indent_guides, cx);
3444
3445                                    let indent_size = params.indent_size;
3446                                    let item_height = params.item_height;
3447
3448                                    params
3449                                        .indent_guides
3450                                        .into_iter()
3451                                        .enumerate()
3452                                        .map(|(idx, layout)| {
3453                                            let offset = if layout.continues_offscreen {
3454                                                px(0.)
3455                                            } else {
3456                                                px(PADDING_Y)
3457                                            };
3458                                            let bounds = Bounds::new(
3459                                                point(
3460                                                    px(layout.offset.x as f32) * indent_size
3461                                                        + px(LEFT_OFFSET),
3462                                                    px(layout.offset.y as f32) * item_height
3463                                                        + offset,
3464                                                ),
3465                                                size(
3466                                                    px(1.),
3467                                                    px(layout.length as f32) * item_height
3468                                                        - px(offset.0 * 2.),
3469                                                ),
3470                                            );
3471                                            ui::RenderedIndentGuide {
3472                                                bounds,
3473                                                layout,
3474                                                is_active: Some(idx) == active_indent_guide_index,
3475                                                hitbox: Some(Bounds::new(
3476                                                    point(
3477                                                        bounds.origin.x - px(HITBOX_OVERDRAW),
3478                                                        bounds.origin.y,
3479                                                    ),
3480                                                    size(
3481                                                        bounds.size.width
3482                                                            + px(2. * HITBOX_OVERDRAW),
3483                                                        bounds.size.height,
3484                                                    ),
3485                                                )),
3486                                            }
3487                                        })
3488                                        .collect()
3489                                },
3490                            ),
3491                        )
3492                    })
3493                    .size_full()
3494                    .with_sizing_behavior(ListSizingBehavior::Infer)
3495                    .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
3496                    .with_width_from_item(self.max_width_item_index)
3497                    .track_scroll(self.scroll_handle.clone()),
3498                )
3499                .children(self.render_vertical_scrollbar(cx))
3500                .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
3501                    this.pb_4().child(scrollbar)
3502                })
3503                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3504                    deferred(
3505                        anchored()
3506                            .position(*position)
3507                            .anchor(gpui::AnchorCorner::TopLeft)
3508                            .child(menu.clone()),
3509                    )
3510                    .with_priority(1)
3511                }))
3512        } else {
3513            v_flex()
3514                .id("empty-project_panel")
3515                .size_full()
3516                .p_4()
3517                .track_focus(&self.focus_handle(cx))
3518                .child(
3519                    Button::new("open_project", "Open a project")
3520                        .full_width()
3521                        .key_binding(KeyBinding::for_action(&workspace::Open, cx))
3522                        .on_click(cx.listener(|this, _, cx| {
3523                            this.workspace
3524                                .update(cx, |_, cx| cx.dispatch_action(Box::new(workspace::Open)))
3525                                .log_err();
3526                        })),
3527                )
3528                .when(is_local, |div| {
3529                    div.drag_over::<ExternalPaths>(|style, _, cx| {
3530                        style.bg(cx.theme().colors().drop_target_background)
3531                    })
3532                    .on_drop(cx.listener(
3533                        move |this, external_paths: &ExternalPaths, cx| {
3534                            this.last_external_paths_drag_over_entry = None;
3535                            this.marked_entries.clear();
3536                            this.hover_scroll_task.take();
3537                            if let Some(task) = this
3538                                .workspace
3539                                .update(cx, |workspace, cx| {
3540                                    workspace.open_workspace_for_paths(
3541                                        true,
3542                                        external_paths.paths().to_owned(),
3543                                        cx,
3544                                    )
3545                                })
3546                                .log_err()
3547                            {
3548                                task.detach_and_log_err(cx);
3549                            }
3550                            cx.stop_propagation();
3551                        },
3552                    ))
3553                })
3554        }
3555    }
3556}
3557
3558impl Render for DraggedProjectEntryView {
3559    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3560        let settings = ProjectPanelSettings::get_global(cx);
3561        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3562        h_flex().font(ui_font).map(|this| {
3563            if self.selections.contains(&self.selection) {
3564                this.flex_shrink()
3565                    .p_1()
3566                    .items_end()
3567                    .rounded_md()
3568                    .child(self.selections.len().to_string())
3569            } else {
3570                this.bg(cx.theme().colors().background).w(self.width).child(
3571                    ListItem::new(self.selection.entry_id.to_proto() as usize)
3572                        .indent_level(self.details.depth)
3573                        .indent_step_size(px(settings.indent_size))
3574                        .child(if let Some(icon) = &self.details.icon {
3575                            div().child(Icon::from_path(icon.clone()))
3576                        } else {
3577                            div()
3578                        })
3579                        .child(Label::new(self.details.filename.clone())),
3580                )
3581            }
3582        })
3583    }
3584}
3585
3586impl EventEmitter<Event> for ProjectPanel {}
3587
3588impl EventEmitter<PanelEvent> for ProjectPanel {}
3589
3590impl Panel for ProjectPanel {
3591    fn position(&self, cx: &WindowContext) -> DockPosition {
3592        match ProjectPanelSettings::get_global(cx).dock {
3593            ProjectPanelDockPosition::Left => DockPosition::Left,
3594            ProjectPanelDockPosition::Right => DockPosition::Right,
3595        }
3596    }
3597
3598    fn position_is_valid(&self, position: DockPosition) -> bool {
3599        matches!(position, DockPosition::Left | DockPosition::Right)
3600    }
3601
3602    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3603        settings::update_settings_file::<ProjectPanelSettings>(
3604            self.fs.clone(),
3605            cx,
3606            move |settings, _| {
3607                let dock = match position {
3608                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
3609                    DockPosition::Right => ProjectPanelDockPosition::Right,
3610                };
3611                settings.dock = Some(dock);
3612            },
3613        );
3614    }
3615
3616    fn size(&self, cx: &WindowContext) -> Pixels {
3617        self.width
3618            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
3619    }
3620
3621    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
3622        self.width = size;
3623        self.serialize(cx);
3624        cx.notify();
3625    }
3626
3627    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
3628        ProjectPanelSettings::get_global(cx)
3629            .button
3630            .then_some(IconName::FileTree)
3631    }
3632
3633    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
3634        Some("Project Panel")
3635    }
3636
3637    fn toggle_action(&self) -> Box<dyn Action> {
3638        Box::new(ToggleFocus)
3639    }
3640
3641    fn persistent_name() -> &'static str {
3642        "Project Panel"
3643    }
3644
3645    fn starts_open(&self, cx: &WindowContext) -> bool {
3646        let project = &self.project.read(cx);
3647        project.visible_worktrees(cx).any(|tree| {
3648            tree.read(cx)
3649                .root_entry()
3650                .map_or(false, |entry| entry.is_dir())
3651        })
3652    }
3653}
3654
3655impl FocusableView for ProjectPanel {
3656    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
3657        self.focus_handle.clone()
3658    }
3659}
3660
3661impl ClipboardEntry {
3662    fn is_cut(&self) -> bool {
3663        matches!(self, Self::Cut { .. })
3664    }
3665
3666    fn items(&self) -> &BTreeSet<SelectedEntry> {
3667        match self {
3668            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
3669        }
3670    }
3671}
3672
3673#[cfg(test)]
3674mod tests {
3675    use super::*;
3676    use collections::HashSet;
3677    use gpui::{Empty, TestAppContext, View, VisualTestContext, WindowHandle};
3678    use pretty_assertions::assert_eq;
3679    use project::{FakeFs, WorktreeSettings};
3680    use serde_json::json;
3681    use settings::SettingsStore;
3682    use std::path::{Path, PathBuf};
3683    use ui::Context;
3684    use workspace::{
3685        item::{Item, ProjectItem},
3686        register_project_item, AppState,
3687    };
3688
3689    #[gpui::test]
3690    async fn test_visible_list(cx: &mut gpui::TestAppContext) {
3691        init_test(cx);
3692
3693        let fs = FakeFs::new(cx.executor().clone());
3694        fs.insert_tree(
3695            "/root1",
3696            json!({
3697                ".dockerignore": "",
3698                ".git": {
3699                    "HEAD": "",
3700                },
3701                "a": {
3702                    "0": { "q": "", "r": "", "s": "" },
3703                    "1": { "t": "", "u": "" },
3704                    "2": { "v": "", "w": "", "x": "", "y": "" },
3705                },
3706                "b": {
3707                    "3": { "Q": "" },
3708                    "4": { "R": "", "S": "", "T": "", "U": "" },
3709                },
3710                "C": {
3711                    "5": {},
3712                    "6": { "V": "", "W": "" },
3713                    "7": { "X": "" },
3714                    "8": { "Y": {}, "Z": "" }
3715                }
3716            }),
3717        )
3718        .await;
3719        fs.insert_tree(
3720            "/root2",
3721            json!({
3722                "d": {
3723                    "9": ""
3724                },
3725                "e": {}
3726            }),
3727        )
3728        .await;
3729
3730        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
3731        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3732        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3733        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3734        assert_eq!(
3735            visible_entries_as_strings(&panel, 0..50, cx),
3736            &[
3737                "v root1",
3738                "    > .git",
3739                "    > a",
3740                "    > b",
3741                "    > C",
3742                "      .dockerignore",
3743                "v root2",
3744                "    > d",
3745                "    > e",
3746            ]
3747        );
3748
3749        toggle_expand_dir(&panel, "root1/b", cx);
3750        assert_eq!(
3751            visible_entries_as_strings(&panel, 0..50, cx),
3752            &[
3753                "v root1",
3754                "    > .git",
3755                "    > a",
3756                "    v b  <== selected",
3757                "        > 3",
3758                "        > 4",
3759                "    > C",
3760                "      .dockerignore",
3761                "v root2",
3762                "    > d",
3763                "    > e",
3764            ]
3765        );
3766
3767        assert_eq!(
3768            visible_entries_as_strings(&panel, 6..9, cx),
3769            &[
3770                //
3771                "    > C",
3772                "      .dockerignore",
3773                "v root2",
3774            ]
3775        );
3776    }
3777
3778    #[gpui::test]
3779    async fn test_opening_file(cx: &mut gpui::TestAppContext) {
3780        init_test_with_editor(cx);
3781
3782        let fs = FakeFs::new(cx.executor().clone());
3783        fs.insert_tree(
3784            "/src",
3785            json!({
3786                "test": {
3787                    "first.rs": "// First Rust file",
3788                    "second.rs": "// Second Rust file",
3789                    "third.rs": "// Third Rust file",
3790                }
3791            }),
3792        )
3793        .await;
3794
3795        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
3796        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3797        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3798        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3799
3800        toggle_expand_dir(&panel, "src/test", cx);
3801        select_path(&panel, "src/test/first.rs", cx);
3802        panel.update(cx, |panel, cx| panel.open(&Open, cx));
3803        cx.executor().run_until_parked();
3804        assert_eq!(
3805            visible_entries_as_strings(&panel, 0..10, cx),
3806            &[
3807                "v src",
3808                "    v test",
3809                "          first.rs  <== selected  <== marked",
3810                "          second.rs",
3811                "          third.rs"
3812            ]
3813        );
3814        ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
3815
3816        select_path(&panel, "src/test/second.rs", cx);
3817        panel.update(cx, |panel, cx| panel.open(&Open, cx));
3818        cx.executor().run_until_parked();
3819        assert_eq!(
3820            visible_entries_as_strings(&panel, 0..10, cx),
3821            &[
3822                "v src",
3823                "    v test",
3824                "          first.rs",
3825                "          second.rs  <== selected  <== marked",
3826                "          third.rs"
3827            ]
3828        );
3829        ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
3830    }
3831
3832    #[gpui::test]
3833    async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
3834        init_test(cx);
3835        cx.update(|cx| {
3836            cx.update_global::<SettingsStore, _>(|store, cx| {
3837                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3838                    worktree_settings.file_scan_exclusions =
3839                        Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
3840                });
3841            });
3842        });
3843
3844        let fs = FakeFs::new(cx.background_executor.clone());
3845        fs.insert_tree(
3846            "/root1",
3847            json!({
3848                ".dockerignore": "",
3849                ".git": {
3850                    "HEAD": "",
3851                },
3852                "a": {
3853                    "0": { "q": "", "r": "", "s": "" },
3854                    "1": { "t": "", "u": "" },
3855                    "2": { "v": "", "w": "", "x": "", "y": "" },
3856                },
3857                "b": {
3858                    "3": { "Q": "" },
3859                    "4": { "R": "", "S": "", "T": "", "U": "" },
3860                },
3861                "C": {
3862                    "5": {},
3863                    "6": { "V": "", "W": "" },
3864                    "7": { "X": "" },
3865                    "8": { "Y": {}, "Z": "" }
3866                }
3867            }),
3868        )
3869        .await;
3870        fs.insert_tree(
3871            "/root2",
3872            json!({
3873                "d": {
3874                    "4": ""
3875                },
3876                "e": {}
3877            }),
3878        )
3879        .await;
3880
3881        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
3882        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3883        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3884        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3885        assert_eq!(
3886            visible_entries_as_strings(&panel, 0..50, cx),
3887            &[
3888                "v root1",
3889                "    > a",
3890                "    > b",
3891                "    > C",
3892                "      .dockerignore",
3893                "v root2",
3894                "    > d",
3895                "    > e",
3896            ]
3897        );
3898
3899        toggle_expand_dir(&panel, "root1/b", cx);
3900        assert_eq!(
3901            visible_entries_as_strings(&panel, 0..50, cx),
3902            &[
3903                "v root1",
3904                "    > a",
3905                "    v b  <== selected",
3906                "        > 3",
3907                "    > C",
3908                "      .dockerignore",
3909                "v root2",
3910                "    > d",
3911                "    > e",
3912            ]
3913        );
3914
3915        toggle_expand_dir(&panel, "root2/d", cx);
3916        assert_eq!(
3917            visible_entries_as_strings(&panel, 0..50, cx),
3918            &[
3919                "v root1",
3920                "    > a",
3921                "    v b",
3922                "        > 3",
3923                "    > C",
3924                "      .dockerignore",
3925                "v root2",
3926                "    v d  <== selected",
3927                "    > e",
3928            ]
3929        );
3930
3931        toggle_expand_dir(&panel, "root2/e", cx);
3932        assert_eq!(
3933            visible_entries_as_strings(&panel, 0..50, cx),
3934            &[
3935                "v root1",
3936                "    > a",
3937                "    v b",
3938                "        > 3",
3939                "    > C",
3940                "      .dockerignore",
3941                "v root2",
3942                "    v d",
3943                "    v e  <== selected",
3944            ]
3945        );
3946    }
3947
3948    #[gpui::test]
3949    async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
3950        init_test(cx);
3951
3952        let fs = FakeFs::new(cx.executor().clone());
3953        fs.insert_tree(
3954            "/root1",
3955            json!({
3956                "dir_1": {
3957                    "nested_dir_1": {
3958                        "nested_dir_2": {
3959                            "nested_dir_3": {
3960                                "file_a.java": "// File contents",
3961                                "file_b.java": "// File contents",
3962                                "file_c.java": "// File contents",
3963                                "nested_dir_4": {
3964                                    "nested_dir_5": {
3965                                        "file_d.java": "// File contents",
3966                                    }
3967                                }
3968                            }
3969                        }
3970                    }
3971                }
3972            }),
3973        )
3974        .await;
3975        fs.insert_tree(
3976            "/root2",
3977            json!({
3978                "dir_2": {
3979                    "file_1.java": "// File contents",
3980                }
3981            }),
3982        )
3983        .await;
3984
3985        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
3986        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3987        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3988        cx.update(|cx| {
3989            let settings = *ProjectPanelSettings::get_global(cx);
3990            ProjectPanelSettings::override_global(
3991                ProjectPanelSettings {
3992                    auto_fold_dirs: true,
3993                    ..settings
3994                },
3995                cx,
3996            );
3997        });
3998        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3999        assert_eq!(
4000            visible_entries_as_strings(&panel, 0..10, cx),
4001            &[
4002                "v root1",
4003                "    > dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
4004                "v root2",
4005                "    > dir_2",
4006            ]
4007        );
4008
4009        toggle_expand_dir(
4010            &panel,
4011            "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
4012            cx,
4013        );
4014        assert_eq!(
4015            visible_entries_as_strings(&panel, 0..10, cx),
4016            &[
4017                "v root1",
4018                "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3  <== selected",
4019                "        > nested_dir_4/nested_dir_5",
4020                "          file_a.java",
4021                "          file_b.java",
4022                "          file_c.java",
4023                "v root2",
4024                "    > dir_2",
4025            ]
4026        );
4027
4028        toggle_expand_dir(
4029            &panel,
4030            "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
4031            cx,
4032        );
4033        assert_eq!(
4034            visible_entries_as_strings(&panel, 0..10, cx),
4035            &[
4036                "v root1",
4037                "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
4038                "        v nested_dir_4/nested_dir_5  <== selected",
4039                "              file_d.java",
4040                "          file_a.java",
4041                "          file_b.java",
4042                "          file_c.java",
4043                "v root2",
4044                "    > dir_2",
4045            ]
4046        );
4047        toggle_expand_dir(&panel, "root2/dir_2", cx);
4048        assert_eq!(
4049            visible_entries_as_strings(&panel, 0..10, cx),
4050            &[
4051                "v root1",
4052                "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
4053                "        v nested_dir_4/nested_dir_5",
4054                "              file_d.java",
4055                "          file_a.java",
4056                "          file_b.java",
4057                "          file_c.java",
4058                "v root2",
4059                "    v dir_2  <== selected",
4060                "          file_1.java",
4061            ]
4062        );
4063    }
4064
4065    #[gpui::test(iterations = 30)]
4066    async fn test_editing_files(cx: &mut gpui::TestAppContext) {
4067        init_test(cx);
4068
4069        let fs = FakeFs::new(cx.executor().clone());
4070        fs.insert_tree(
4071            "/root1",
4072            json!({
4073                ".dockerignore": "",
4074                ".git": {
4075                    "HEAD": "",
4076                },
4077                "a": {
4078                    "0": { "q": "", "r": "", "s": "" },
4079                    "1": { "t": "", "u": "" },
4080                    "2": { "v": "", "w": "", "x": "", "y": "" },
4081                },
4082                "b": {
4083                    "3": { "Q": "" },
4084                    "4": { "R": "", "S": "", "T": "", "U": "" },
4085                },
4086                "C": {
4087                    "5": {},
4088                    "6": { "V": "", "W": "" },
4089                    "7": { "X": "" },
4090                    "8": { "Y": {}, "Z": "" }
4091                }
4092            }),
4093        )
4094        .await;
4095        fs.insert_tree(
4096            "/root2",
4097            json!({
4098                "d": {
4099                    "9": ""
4100                },
4101                "e": {}
4102            }),
4103        )
4104        .await;
4105
4106        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4107        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4108        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4109        let panel = workspace
4110            .update(cx, |workspace, cx| {
4111                let panel = ProjectPanel::new(workspace, cx);
4112                workspace.add_panel(panel.clone(), cx);
4113                panel
4114            })
4115            .unwrap();
4116
4117        select_path(&panel, "root1", cx);
4118        assert_eq!(
4119            visible_entries_as_strings(&panel, 0..10, cx),
4120            &[
4121                "v root1  <== selected",
4122                "    > .git",
4123                "    > a",
4124                "    > b",
4125                "    > C",
4126                "      .dockerignore",
4127                "v root2",
4128                "    > d",
4129                "    > e",
4130            ]
4131        );
4132
4133        // Add a file with the root folder selected. The filename editor is placed
4134        // before the first file in the root folder.
4135        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
4136        panel.update(cx, |panel, cx| {
4137            assert!(panel.filename_editor.read(cx).is_focused(cx));
4138        });
4139        assert_eq!(
4140            visible_entries_as_strings(&panel, 0..10, cx),
4141            &[
4142                "v root1",
4143                "    > .git",
4144                "    > a",
4145                "    > b",
4146                "    > C",
4147                "      [EDITOR: '']  <== selected",
4148                "      .dockerignore",
4149                "v root2",
4150                "    > d",
4151                "    > e",
4152            ]
4153        );
4154
4155        let confirm = panel.update(cx, |panel, cx| {
4156            panel
4157                .filename_editor
4158                .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
4159            panel.confirm_edit(cx).unwrap()
4160        });
4161        assert_eq!(
4162            visible_entries_as_strings(&panel, 0..10, cx),
4163            &[
4164                "v root1",
4165                "    > .git",
4166                "    > a",
4167                "    > b",
4168                "    > C",
4169                "      [PROCESSING: 'the-new-filename']  <== selected",
4170                "      .dockerignore",
4171                "v root2",
4172                "    > d",
4173                "    > e",
4174            ]
4175        );
4176
4177        confirm.await.unwrap();
4178        assert_eq!(
4179            visible_entries_as_strings(&panel, 0..10, cx),
4180            &[
4181                "v root1",
4182                "    > .git",
4183                "    > a",
4184                "    > b",
4185                "    > C",
4186                "      .dockerignore",
4187                "      the-new-filename  <== selected  <== marked",
4188                "v root2",
4189                "    > d",
4190                "    > e",
4191            ]
4192        );
4193
4194        select_path(&panel, "root1/b", cx);
4195        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
4196        assert_eq!(
4197            visible_entries_as_strings(&panel, 0..10, cx),
4198            &[
4199                "v root1",
4200                "    > .git",
4201                "    > a",
4202                "    v b",
4203                "        > 3",
4204                "        > 4",
4205                "          [EDITOR: '']  <== selected",
4206                "    > C",
4207                "      .dockerignore",
4208                "      the-new-filename",
4209            ]
4210        );
4211
4212        panel
4213            .update(cx, |panel, cx| {
4214                panel
4215                    .filename_editor
4216                    .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx));
4217                panel.confirm_edit(cx).unwrap()
4218            })
4219            .await
4220            .unwrap();
4221        assert_eq!(
4222            visible_entries_as_strings(&panel, 0..10, cx),
4223            &[
4224                "v root1",
4225                "    > .git",
4226                "    > a",
4227                "    v b",
4228                "        > 3",
4229                "        > 4",
4230                "          another-filename.txt  <== selected  <== marked",
4231                "    > C",
4232                "      .dockerignore",
4233                "      the-new-filename",
4234            ]
4235        );
4236
4237        select_path(&panel, "root1/b/another-filename.txt", cx);
4238        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
4239        assert_eq!(
4240            visible_entries_as_strings(&panel, 0..10, cx),
4241            &[
4242                "v root1",
4243                "    > .git",
4244                "    > a",
4245                "    v b",
4246                "        > 3",
4247                "        > 4",
4248                "          [EDITOR: 'another-filename.txt']  <== selected  <== marked",
4249                "    > C",
4250                "      .dockerignore",
4251                "      the-new-filename",
4252            ]
4253        );
4254
4255        let confirm = panel.update(cx, |panel, cx| {
4256            panel.filename_editor.update(cx, |editor, cx| {
4257                let file_name_selections = editor.selections.all::<usize>(cx);
4258                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
4259                let file_name_selection = &file_name_selections[0];
4260                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
4261                assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
4262
4263                editor.set_text("a-different-filename.tar.gz", cx)
4264            });
4265            panel.confirm_edit(cx).unwrap()
4266        });
4267        assert_eq!(
4268            visible_entries_as_strings(&panel, 0..10, cx),
4269            &[
4270                "v root1",
4271                "    > .git",
4272                "    > a",
4273                "    v b",
4274                "        > 3",
4275                "        > 4",
4276                "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected  <== marked",
4277                "    > C",
4278                "      .dockerignore",
4279                "      the-new-filename",
4280            ]
4281        );
4282
4283        confirm.await.unwrap();
4284        assert_eq!(
4285            visible_entries_as_strings(&panel, 0..10, cx),
4286            &[
4287                "v root1",
4288                "    > .git",
4289                "    > a",
4290                "    v b",
4291                "        > 3",
4292                "        > 4",
4293                "          a-different-filename.tar.gz  <== selected",
4294                "    > C",
4295                "      .dockerignore",
4296                "      the-new-filename",
4297            ]
4298        );
4299
4300        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
4301        assert_eq!(
4302            visible_entries_as_strings(&panel, 0..10, cx),
4303            &[
4304                "v root1",
4305                "    > .git",
4306                "    > a",
4307                "    v b",
4308                "        > 3",
4309                "        > 4",
4310                "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
4311                "    > C",
4312                "      .dockerignore",
4313                "      the-new-filename",
4314            ]
4315        );
4316
4317        panel.update(cx, |panel, cx| {
4318            panel.filename_editor.update(cx, |editor, cx| {
4319                let file_name_selections = editor.selections.all::<usize>(cx);
4320                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
4321                let file_name_selection = &file_name_selections[0];
4322                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
4323                assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot..");
4324
4325            });
4326            panel.cancel(&menu::Cancel, cx)
4327        });
4328
4329        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
4330        assert_eq!(
4331            visible_entries_as_strings(&panel, 0..10, cx),
4332            &[
4333                "v root1",
4334                "    > .git",
4335                "    > a",
4336                "    v b",
4337                "        > 3",
4338                "        > 4",
4339                "        > [EDITOR: '']  <== selected",
4340                "          a-different-filename.tar.gz",
4341                "    > C",
4342                "      .dockerignore",
4343            ]
4344        );
4345
4346        let confirm = panel.update(cx, |panel, cx| {
4347            panel
4348                .filename_editor
4349                .update(cx, |editor, cx| editor.set_text("new-dir", cx));
4350            panel.confirm_edit(cx).unwrap()
4351        });
4352        panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
4353        assert_eq!(
4354            visible_entries_as_strings(&panel, 0..10, cx),
4355            &[
4356                "v root1",
4357                "    > .git",
4358                "    > a",
4359                "    v b",
4360                "        > 3",
4361                "        > 4",
4362                "        > [PROCESSING: 'new-dir']",
4363                "          a-different-filename.tar.gz  <== selected",
4364                "    > C",
4365                "      .dockerignore",
4366            ]
4367        );
4368
4369        confirm.await.unwrap();
4370        assert_eq!(
4371            visible_entries_as_strings(&panel, 0..10, cx),
4372            &[
4373                "v root1",
4374                "    > .git",
4375                "    > a",
4376                "    v b",
4377                "        > 3",
4378                "        > 4",
4379                "        > new-dir",
4380                "          a-different-filename.tar.gz  <== selected",
4381                "    > C",
4382                "      .dockerignore",
4383            ]
4384        );
4385
4386        panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx));
4387        assert_eq!(
4388            visible_entries_as_strings(&panel, 0..10, cx),
4389            &[
4390                "v root1",
4391                "    > .git",
4392                "    > a",
4393                "    v b",
4394                "        > 3",
4395                "        > 4",
4396                "        > new-dir",
4397                "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
4398                "    > C",
4399                "      .dockerignore",
4400            ]
4401        );
4402
4403        // Dismiss the rename editor when it loses focus.
4404        workspace.update(cx, |_, cx| cx.blur()).unwrap();
4405        assert_eq!(
4406            visible_entries_as_strings(&panel, 0..10, cx),
4407            &[
4408                "v root1",
4409                "    > .git",
4410                "    > a",
4411                "    v b",
4412                "        > 3",
4413                "        > 4",
4414                "        > new-dir",
4415                "          a-different-filename.tar.gz  <== selected",
4416                "    > C",
4417                "      .dockerignore",
4418            ]
4419        );
4420    }
4421
4422    #[gpui::test(iterations = 10)]
4423    async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
4424        init_test(cx);
4425
4426        let fs = FakeFs::new(cx.executor().clone());
4427        fs.insert_tree(
4428            "/root1",
4429            json!({
4430                ".dockerignore": "",
4431                ".git": {
4432                    "HEAD": "",
4433                },
4434                "a": {
4435                    "0": { "q": "", "r": "", "s": "" },
4436                    "1": { "t": "", "u": "" },
4437                    "2": { "v": "", "w": "", "x": "", "y": "" },
4438                },
4439                "b": {
4440                    "3": { "Q": "" },
4441                    "4": { "R": "", "S": "", "T": "", "U": "" },
4442                },
4443                "C": {
4444                    "5": {},
4445                    "6": { "V": "", "W": "" },
4446                    "7": { "X": "" },
4447                    "8": { "Y": {}, "Z": "" }
4448                }
4449            }),
4450        )
4451        .await;
4452        fs.insert_tree(
4453            "/root2",
4454            json!({
4455                "d": {
4456                    "9": ""
4457                },
4458                "e": {}
4459            }),
4460        )
4461        .await;
4462
4463        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4464        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4465        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4466        let panel = workspace
4467            .update(cx, |workspace, cx| {
4468                let panel = ProjectPanel::new(workspace, cx);
4469                workspace.add_panel(panel.clone(), cx);
4470                panel
4471            })
4472            .unwrap();
4473
4474        select_path(&panel, "root1", cx);
4475        assert_eq!(
4476            visible_entries_as_strings(&panel, 0..10, cx),
4477            &[
4478                "v root1  <== selected",
4479                "    > .git",
4480                "    > a",
4481                "    > b",
4482                "    > C",
4483                "      .dockerignore",
4484                "v root2",
4485                "    > d",
4486                "    > e",
4487            ]
4488        );
4489
4490        // Add a file with the root folder selected. The filename editor is placed
4491        // before the first file in the root folder.
4492        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
4493        panel.update(cx, |panel, cx| {
4494            assert!(panel.filename_editor.read(cx).is_focused(cx));
4495        });
4496        assert_eq!(
4497            visible_entries_as_strings(&panel, 0..10, cx),
4498            &[
4499                "v root1",
4500                "    > .git",
4501                "    > a",
4502                "    > b",
4503                "    > C",
4504                "      [EDITOR: '']  <== selected",
4505                "      .dockerignore",
4506                "v root2",
4507                "    > d",
4508                "    > e",
4509            ]
4510        );
4511
4512        let confirm = panel.update(cx, |panel, cx| {
4513            panel.filename_editor.update(cx, |editor, cx| {
4514                editor.set_text("/bdir1/dir2/the-new-filename", cx)
4515            });
4516            panel.confirm_edit(cx).unwrap()
4517        });
4518
4519        assert_eq!(
4520            visible_entries_as_strings(&panel, 0..10, cx),
4521            &[
4522                "v root1",
4523                "    > .git",
4524                "    > a",
4525                "    > b",
4526                "    > C",
4527                "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
4528                "      .dockerignore",
4529                "v root2",
4530                "    > d",
4531                "    > e",
4532            ]
4533        );
4534
4535        confirm.await.unwrap();
4536        assert_eq!(
4537            visible_entries_as_strings(&panel, 0..13, cx),
4538            &[
4539                "v root1",
4540                "    > .git",
4541                "    > a",
4542                "    > b",
4543                "    v bdir1",
4544                "        v dir2",
4545                "              the-new-filename  <== selected  <== marked",
4546                "    > C",
4547                "      .dockerignore",
4548                "v root2",
4549                "    > d",
4550                "    > e",
4551            ]
4552        );
4553    }
4554
4555    #[gpui::test]
4556    async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
4557        init_test(cx);
4558
4559        let fs = FakeFs::new(cx.executor().clone());
4560        fs.insert_tree(
4561            "/root1",
4562            json!({
4563                ".dockerignore": "",
4564                ".git": {
4565                    "HEAD": "",
4566                },
4567            }),
4568        )
4569        .await;
4570
4571        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
4572        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4573        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4574        let panel = workspace
4575            .update(cx, |workspace, cx| {
4576                let panel = ProjectPanel::new(workspace, cx);
4577                workspace.add_panel(panel.clone(), cx);
4578                panel
4579            })
4580            .unwrap();
4581
4582        select_path(&panel, "root1", cx);
4583        assert_eq!(
4584            visible_entries_as_strings(&panel, 0..10, cx),
4585            &["v root1  <== selected", "    > .git", "      .dockerignore",]
4586        );
4587
4588        // Add a file with the root folder selected. The filename editor is placed
4589        // before the first file in the root folder.
4590        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
4591        panel.update(cx, |panel, cx| {
4592            assert!(panel.filename_editor.read(cx).is_focused(cx));
4593        });
4594        assert_eq!(
4595            visible_entries_as_strings(&panel, 0..10, cx),
4596            &[
4597                "v root1",
4598                "    > .git",
4599                "      [EDITOR: '']  <== selected",
4600                "      .dockerignore",
4601            ]
4602        );
4603
4604        let confirm = panel.update(cx, |panel, cx| {
4605            panel
4606                .filename_editor
4607                .update(cx, |editor, cx| editor.set_text("/new_dir/", cx));
4608            panel.confirm_edit(cx).unwrap()
4609        });
4610
4611        assert_eq!(
4612            visible_entries_as_strings(&panel, 0..10, cx),
4613            &[
4614                "v root1",
4615                "    > .git",
4616                "      [PROCESSING: '/new_dir/']  <== selected",
4617                "      .dockerignore",
4618            ]
4619        );
4620
4621        confirm.await.unwrap();
4622        assert_eq!(
4623            visible_entries_as_strings(&panel, 0..13, cx),
4624            &[
4625                "v root1",
4626                "    > .git",
4627                "    v new_dir  <== selected",
4628                "      .dockerignore",
4629            ]
4630        );
4631    }
4632
4633    #[gpui::test]
4634    async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
4635        init_test(cx);
4636
4637        let fs = FakeFs::new(cx.executor().clone());
4638        fs.insert_tree(
4639            "/root1",
4640            json!({
4641                "one.two.txt": "",
4642                "one.txt": ""
4643            }),
4644        )
4645        .await;
4646
4647        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
4648        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4649        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4650        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4651
4652        panel.update(cx, |panel, cx| {
4653            panel.select_next(&Default::default(), cx);
4654            panel.select_next(&Default::default(), cx);
4655        });
4656
4657        assert_eq!(
4658            visible_entries_as_strings(&panel, 0..50, cx),
4659            &[
4660                //
4661                "v root1",
4662                "      one.txt  <== selected",
4663                "      one.two.txt",
4664            ]
4665        );
4666
4667        // Regression test - file name is created correctly when
4668        // the copied file's name contains multiple dots.
4669        panel.update(cx, |panel, cx| {
4670            panel.copy(&Default::default(), cx);
4671            panel.paste(&Default::default(), cx);
4672        });
4673        cx.executor().run_until_parked();
4674
4675        assert_eq!(
4676            visible_entries_as_strings(&panel, 0..50, cx),
4677            &[
4678                //
4679                "v root1",
4680                "      one.txt",
4681                "      one copy.txt  <== selected",
4682                "      one.two.txt",
4683            ]
4684        );
4685
4686        panel.update(cx, |panel, cx| {
4687            panel.paste(&Default::default(), cx);
4688        });
4689        cx.executor().run_until_parked();
4690
4691        assert_eq!(
4692            visible_entries_as_strings(&panel, 0..50, cx),
4693            &[
4694                //
4695                "v root1",
4696                "      one.txt",
4697                "      one copy.txt",
4698                "      one copy 1.txt  <== selected",
4699                "      one.two.txt",
4700            ]
4701        );
4702    }
4703
4704    #[gpui::test]
4705    async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
4706        init_test(cx);
4707
4708        let fs = FakeFs::new(cx.executor().clone());
4709        fs.insert_tree(
4710            "/root1",
4711            json!({
4712                "one.txt": "",
4713                "two.txt": "",
4714                "three.txt": "",
4715                "a": {
4716                    "0": { "q": "", "r": "", "s": "" },
4717                    "1": { "t": "", "u": "" },
4718                    "2": { "v": "", "w": "", "x": "", "y": "" },
4719                },
4720            }),
4721        )
4722        .await;
4723
4724        fs.insert_tree(
4725            "/root2",
4726            json!({
4727                "one.txt": "",
4728                "two.txt": "",
4729                "four.txt": "",
4730                "b": {
4731                    "3": { "Q": "" },
4732                    "4": { "R": "", "S": "", "T": "", "U": "" },
4733                },
4734            }),
4735        )
4736        .await;
4737
4738        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4739        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4740        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4741        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4742
4743        select_path(&panel, "root1/three.txt", cx);
4744        panel.update(cx, |panel, cx| {
4745            panel.cut(&Default::default(), cx);
4746        });
4747
4748        select_path(&panel, "root2/one.txt", cx);
4749        panel.update(cx, |panel, cx| {
4750            panel.select_next(&Default::default(), cx);
4751            panel.paste(&Default::default(), cx);
4752        });
4753        cx.executor().run_until_parked();
4754        assert_eq!(
4755            visible_entries_as_strings(&panel, 0..50, cx),
4756            &[
4757                //
4758                "v root1",
4759                "    > a",
4760                "      one.txt",
4761                "      two.txt",
4762                "v root2",
4763                "    > b",
4764                "      four.txt",
4765                "      one.txt",
4766                "      three.txt  <== selected",
4767                "      two.txt",
4768            ]
4769        );
4770
4771        select_path(&panel, "root1/a", cx);
4772        panel.update(cx, |panel, cx| {
4773            panel.cut(&Default::default(), cx);
4774        });
4775        select_path(&panel, "root2/two.txt", cx);
4776        panel.update(cx, |panel, cx| {
4777            panel.select_next(&Default::default(), cx);
4778            panel.paste(&Default::default(), cx);
4779        });
4780
4781        cx.executor().run_until_parked();
4782        assert_eq!(
4783            visible_entries_as_strings(&panel, 0..50, cx),
4784            &[
4785                //
4786                "v root1",
4787                "      one.txt",
4788                "      two.txt",
4789                "v root2",
4790                "    > a  <== selected",
4791                "    > b",
4792                "      four.txt",
4793                "      one.txt",
4794                "      three.txt",
4795                "      two.txt",
4796            ]
4797        );
4798    }
4799
4800    #[gpui::test]
4801    async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
4802        init_test(cx);
4803
4804        let fs = FakeFs::new(cx.executor().clone());
4805        fs.insert_tree(
4806            "/root1",
4807            json!({
4808                "one.txt": "",
4809                "two.txt": "",
4810                "three.txt": "",
4811                "a": {
4812                    "0": { "q": "", "r": "", "s": "" },
4813                    "1": { "t": "", "u": "" },
4814                    "2": { "v": "", "w": "", "x": "", "y": "" },
4815                },
4816            }),
4817        )
4818        .await;
4819
4820        fs.insert_tree(
4821            "/root2",
4822            json!({
4823                "one.txt": "",
4824                "two.txt": "",
4825                "four.txt": "",
4826                "b": {
4827                    "3": { "Q": "" },
4828                    "4": { "R": "", "S": "", "T": "", "U": "" },
4829                },
4830            }),
4831        )
4832        .await;
4833
4834        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4835        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4836        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4837        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4838
4839        select_path(&panel, "root1/three.txt", cx);
4840        panel.update(cx, |panel, cx| {
4841            panel.copy(&Default::default(), cx);
4842        });
4843
4844        select_path(&panel, "root2/one.txt", cx);
4845        panel.update(cx, |panel, cx| {
4846            panel.select_next(&Default::default(), cx);
4847            panel.paste(&Default::default(), cx);
4848        });
4849        cx.executor().run_until_parked();
4850        assert_eq!(
4851            visible_entries_as_strings(&panel, 0..50, cx),
4852            &[
4853                //
4854                "v root1",
4855                "    > a",
4856                "      one.txt",
4857                "      three.txt",
4858                "      two.txt",
4859                "v root2",
4860                "    > b",
4861                "      four.txt",
4862                "      one.txt",
4863                "      three.txt  <== selected",
4864                "      two.txt",
4865            ]
4866        );
4867
4868        select_path(&panel, "root1/three.txt", cx);
4869        panel.update(cx, |panel, cx| {
4870            panel.copy(&Default::default(), cx);
4871        });
4872        select_path(&panel, "root2/two.txt", cx);
4873        panel.update(cx, |panel, cx| {
4874            panel.select_next(&Default::default(), cx);
4875            panel.paste(&Default::default(), cx);
4876        });
4877
4878        cx.executor().run_until_parked();
4879        assert_eq!(
4880            visible_entries_as_strings(&panel, 0..50, cx),
4881            &[
4882                //
4883                "v root1",
4884                "    > a",
4885                "      one.txt",
4886                "      three.txt",
4887                "      two.txt",
4888                "v root2",
4889                "    > b",
4890                "      four.txt",
4891                "      one.txt",
4892                "      three.txt",
4893                "      three copy.txt  <== selected",
4894                "      two.txt",
4895            ]
4896        );
4897
4898        select_path(&panel, "root1/a", cx);
4899        panel.update(cx, |panel, cx| {
4900            panel.copy(&Default::default(), cx);
4901        });
4902        select_path(&panel, "root2/two.txt", cx);
4903        panel.update(cx, |panel, cx| {
4904            panel.select_next(&Default::default(), cx);
4905            panel.paste(&Default::default(), cx);
4906        });
4907
4908        cx.executor().run_until_parked();
4909        assert_eq!(
4910            visible_entries_as_strings(&panel, 0..50, cx),
4911            &[
4912                //
4913                "v root1",
4914                "    > a",
4915                "      one.txt",
4916                "      three.txt",
4917                "      two.txt",
4918                "v root2",
4919                "    > a  <== selected",
4920                "    > b",
4921                "      four.txt",
4922                "      one.txt",
4923                "      three.txt",
4924                "      three copy.txt",
4925                "      two.txt",
4926            ]
4927        );
4928    }
4929
4930    #[gpui::test]
4931    async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) {
4932        init_test(cx);
4933
4934        let fs = FakeFs::new(cx.executor().clone());
4935        fs.insert_tree(
4936            "/root",
4937            json!({
4938                "a": {
4939                    "one.txt": "",
4940                    "two.txt": "",
4941                    "inner_dir": {
4942                        "three.txt": "",
4943                        "four.txt": "",
4944                    }
4945                },
4946                "b": {}
4947            }),
4948        )
4949        .await;
4950
4951        let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4952        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4953        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4954        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4955
4956        select_path(&panel, "root/a", cx);
4957        panel.update(cx, |panel, cx| {
4958            panel.copy(&Default::default(), cx);
4959            panel.select_next(&Default::default(), cx);
4960            panel.paste(&Default::default(), cx);
4961        });
4962        cx.executor().run_until_parked();
4963
4964        let pasted_dir = find_project_entry(&panel, "root/b/a", cx);
4965        assert_ne!(pasted_dir, None, "Pasted directory should have an entry");
4966
4967        let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx);
4968        assert_ne!(
4969            pasted_dir_file, None,
4970            "Pasted directory file should have an entry"
4971        );
4972
4973        let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx);
4974        assert_ne!(
4975            pasted_dir_inner_dir, None,
4976            "Directories inside pasted directory should have an entry"
4977        );
4978
4979        toggle_expand_dir(&panel, "root/b/a", cx);
4980        toggle_expand_dir(&panel, "root/b/a/inner_dir", cx);
4981
4982        assert_eq!(
4983            visible_entries_as_strings(&panel, 0..50, cx),
4984            &[
4985                //
4986                "v root",
4987                "    > a",
4988                "    v b",
4989                "        v a",
4990                "            v inner_dir  <== selected",
4991                "                  four.txt",
4992                "                  three.txt",
4993                "              one.txt",
4994                "              two.txt",
4995            ]
4996        );
4997
4998        select_path(&panel, "root", cx);
4999        panel.update(cx, |panel, cx| panel.paste(&Default::default(), cx));
5000        cx.executor().run_until_parked();
5001        panel.update(cx, |panel, cx| panel.paste(&Default::default(), cx));
5002        cx.executor().run_until_parked();
5003        assert_eq!(
5004            visible_entries_as_strings(&panel, 0..50, cx),
5005            &[
5006                //
5007                "v root",
5008                "    > a",
5009                "    v a copy",
5010                "        > a  <== selected",
5011                "        > inner_dir",
5012                "          one.txt",
5013                "          two.txt",
5014                "    v b",
5015                "        v a",
5016                "            v inner_dir",
5017                "                  four.txt",
5018                "                  three.txt",
5019                "              one.txt",
5020                "              two.txt"
5021            ]
5022        );
5023    }
5024
5025    #[gpui::test]
5026    async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
5027        init_test_with_editor(cx);
5028
5029        let fs = FakeFs::new(cx.executor().clone());
5030        fs.insert_tree(
5031            "/src",
5032            json!({
5033                "test": {
5034                    "first.rs": "// First Rust file",
5035                    "second.rs": "// Second Rust file",
5036                    "third.rs": "// Third Rust file",
5037                }
5038            }),
5039        )
5040        .await;
5041
5042        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
5043        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5044        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5045        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5046
5047        toggle_expand_dir(&panel, "src/test", cx);
5048        select_path(&panel, "src/test/first.rs", cx);
5049        panel.update(cx, |panel, cx| panel.open(&Open, cx));
5050        cx.executor().run_until_parked();
5051        assert_eq!(
5052            visible_entries_as_strings(&panel, 0..10, cx),
5053            &[
5054                "v src",
5055                "    v test",
5056                "          first.rs  <== selected  <== marked",
5057                "          second.rs",
5058                "          third.rs"
5059            ]
5060        );
5061        ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
5062
5063        submit_deletion(&panel, cx);
5064        assert_eq!(
5065            visible_entries_as_strings(&panel, 0..10, cx),
5066            &[
5067                "v src",
5068                "    v test",
5069                "          second.rs",
5070                "          third.rs"
5071            ],
5072            "Project panel should have no deleted file, no other file is selected in it"
5073        );
5074        ensure_no_open_items_and_panes(&workspace, cx);
5075
5076        select_path(&panel, "src/test/second.rs", cx);
5077        panel.update(cx, |panel, cx| panel.open(&Open, cx));
5078        cx.executor().run_until_parked();
5079        assert_eq!(
5080            visible_entries_as_strings(&panel, 0..10, cx),
5081            &[
5082                "v src",
5083                "    v test",
5084                "          second.rs  <== selected  <== marked",
5085                "          third.rs"
5086            ]
5087        );
5088        ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
5089
5090        workspace
5091            .update(cx, |workspace, cx| {
5092                let active_items = workspace
5093                    .panes()
5094                    .iter()
5095                    .filter_map(|pane| pane.read(cx).active_item())
5096                    .collect::<Vec<_>>();
5097                assert_eq!(active_items.len(), 1);
5098                let open_editor = active_items
5099                    .into_iter()
5100                    .next()
5101                    .unwrap()
5102                    .downcast::<Editor>()
5103                    .expect("Open item should be an editor");
5104                open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
5105            })
5106            .unwrap();
5107        submit_deletion_skipping_prompt(&panel, cx);
5108        assert_eq!(
5109            visible_entries_as_strings(&panel, 0..10, cx),
5110            &["v src", "    v test", "          third.rs"],
5111            "Project panel should have no deleted file, with one last file remaining"
5112        );
5113        ensure_no_open_items_and_panes(&workspace, cx);
5114    }
5115
5116    #[gpui::test]
5117    async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
5118        init_test_with_editor(cx);
5119
5120        let fs = FakeFs::new(cx.executor().clone());
5121        fs.insert_tree(
5122            "/src",
5123            json!({
5124                "test": {
5125                    "first.rs": "// First Rust file",
5126                    "second.rs": "// Second Rust file",
5127                    "third.rs": "// Third Rust file",
5128                }
5129            }),
5130        )
5131        .await;
5132
5133        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
5134        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5135        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5136        let panel = workspace
5137            .update(cx, |workspace, cx| {
5138                let panel = ProjectPanel::new(workspace, cx);
5139                workspace.add_panel(panel.clone(), cx);
5140                panel
5141            })
5142            .unwrap();
5143
5144        select_path(&panel, "src/", cx);
5145        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
5146        cx.executor().run_until_parked();
5147        assert_eq!(
5148            visible_entries_as_strings(&panel, 0..10, cx),
5149            &[
5150                //
5151                "v src  <== selected",
5152                "    > test"
5153            ]
5154        );
5155        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
5156        panel.update(cx, |panel, cx| {
5157            assert!(panel.filename_editor.read(cx).is_focused(cx));
5158        });
5159        assert_eq!(
5160            visible_entries_as_strings(&panel, 0..10, cx),
5161            &[
5162                //
5163                "v src",
5164                "    > [EDITOR: '']  <== selected",
5165                "    > test"
5166            ]
5167        );
5168        panel.update(cx, |panel, cx| {
5169            panel
5170                .filename_editor
5171                .update(cx, |editor, cx| editor.set_text("test", cx));
5172            assert!(
5173                panel.confirm_edit(cx).is_none(),
5174                "Should not allow to confirm on conflicting new directory name"
5175            )
5176        });
5177        assert_eq!(
5178            visible_entries_as_strings(&panel, 0..10, cx),
5179            &[
5180                //
5181                "v src",
5182                "    > test"
5183            ],
5184            "File list should be unchanged after failed folder create confirmation"
5185        );
5186
5187        select_path(&panel, "src/test/", cx);
5188        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
5189        cx.executor().run_until_parked();
5190        assert_eq!(
5191            visible_entries_as_strings(&panel, 0..10, cx),
5192            &[
5193                //
5194                "v src",
5195                "    > test  <== selected"
5196            ]
5197        );
5198        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
5199        panel.update(cx, |panel, cx| {
5200            assert!(panel.filename_editor.read(cx).is_focused(cx));
5201        });
5202        assert_eq!(
5203            visible_entries_as_strings(&panel, 0..10, cx),
5204            &[
5205                "v src",
5206                "    v test",
5207                "          [EDITOR: '']  <== selected",
5208                "          first.rs",
5209                "          second.rs",
5210                "          third.rs"
5211            ]
5212        );
5213        panel.update(cx, |panel, cx| {
5214            panel
5215                .filename_editor
5216                .update(cx, |editor, cx| editor.set_text("first.rs", cx));
5217            assert!(
5218                panel.confirm_edit(cx).is_none(),
5219                "Should not allow to confirm on conflicting new file name"
5220            )
5221        });
5222        assert_eq!(
5223            visible_entries_as_strings(&panel, 0..10, cx),
5224            &[
5225                "v src",
5226                "    v test",
5227                "          first.rs",
5228                "          second.rs",
5229                "          third.rs"
5230            ],
5231            "File list should be unchanged after failed file create confirmation"
5232        );
5233
5234        select_path(&panel, "src/test/first.rs", cx);
5235        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
5236        cx.executor().run_until_parked();
5237        assert_eq!(
5238            visible_entries_as_strings(&panel, 0..10, cx),
5239            &[
5240                "v src",
5241                "    v test",
5242                "          first.rs  <== selected",
5243                "          second.rs",
5244                "          third.rs"
5245            ],
5246        );
5247        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
5248        panel.update(cx, |panel, cx| {
5249            assert!(panel.filename_editor.read(cx).is_focused(cx));
5250        });
5251        assert_eq!(
5252            visible_entries_as_strings(&panel, 0..10, cx),
5253            &[
5254                "v src",
5255                "    v test",
5256                "          [EDITOR: 'first.rs']  <== selected",
5257                "          second.rs",
5258                "          third.rs"
5259            ]
5260        );
5261        panel.update(cx, |panel, cx| {
5262            panel
5263                .filename_editor
5264                .update(cx, |editor, cx| editor.set_text("second.rs", cx));
5265            assert!(
5266                panel.confirm_edit(cx).is_none(),
5267                "Should not allow to confirm on conflicting file rename"
5268            )
5269        });
5270        assert_eq!(
5271            visible_entries_as_strings(&panel, 0..10, cx),
5272            &[
5273                "v src",
5274                "    v test",
5275                "          first.rs  <== selected",
5276                "          second.rs",
5277                "          third.rs"
5278            ],
5279            "File list should be unchanged after failed rename confirmation"
5280        );
5281    }
5282
5283    #[gpui::test]
5284    async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
5285        init_test_with_editor(cx);
5286
5287        let fs = FakeFs::new(cx.executor().clone());
5288        fs.insert_tree(
5289            "/project_root",
5290            json!({
5291                "dir_1": {
5292                    "nested_dir": {
5293                        "file_a.py": "# File contents",
5294                    }
5295                },
5296                "file_1.py": "# File contents",
5297            }),
5298        )
5299        .await;
5300
5301        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
5302        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5303        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5304        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5305
5306        panel.update(cx, |panel, cx| panel.open(&Open, cx));
5307        cx.executor().run_until_parked();
5308        select_path(&panel, "project_root/dir_1", cx);
5309        panel.update(cx, |panel, cx| panel.open(&Open, cx));
5310        select_path(&panel, "project_root/dir_1/nested_dir", cx);
5311        panel.update(cx, |panel, cx| panel.open(&Open, cx));
5312        panel.update(cx, |panel, cx| panel.open(&Open, cx));
5313        cx.executor().run_until_parked();
5314        assert_eq!(
5315            visible_entries_as_strings(&panel, 0..10, cx),
5316            &[
5317                "v project_root",
5318                "    v dir_1",
5319                "        > nested_dir  <== selected",
5320                "      file_1.py",
5321            ]
5322        );
5323    }
5324
5325    #[gpui::test]
5326    async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
5327        init_test_with_editor(cx);
5328
5329        let fs = FakeFs::new(cx.executor().clone());
5330        fs.insert_tree(
5331            "/project_root",
5332            json!({
5333                "dir_1": {
5334                    "nested_dir": {
5335                        "file_a.py": "# File contents",
5336                        "file_b.py": "# File contents",
5337                        "file_c.py": "# File contents",
5338                    },
5339                    "file_1.py": "# File contents",
5340                    "file_2.py": "# File contents",
5341                    "file_3.py": "# File contents",
5342                },
5343                "dir_2": {
5344                    "file_1.py": "# File contents",
5345                    "file_2.py": "# File contents",
5346                    "file_3.py": "# File contents",
5347                }
5348            }),
5349        )
5350        .await;
5351
5352        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
5353        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5354        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5355        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5356
5357        panel.update(cx, |panel, cx| {
5358            panel.collapse_all_entries(&CollapseAllEntries, cx)
5359        });
5360        cx.executor().run_until_parked();
5361        assert_eq!(
5362            visible_entries_as_strings(&panel, 0..10, cx),
5363            &["v project_root", "    > dir_1", "    > dir_2",]
5364        );
5365
5366        // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
5367        toggle_expand_dir(&panel, "project_root/dir_1", cx);
5368        cx.executor().run_until_parked();
5369        assert_eq!(
5370            visible_entries_as_strings(&panel, 0..10, cx),
5371            &[
5372                "v project_root",
5373                "    v dir_1  <== selected",
5374                "        > nested_dir",
5375                "          file_1.py",
5376                "          file_2.py",
5377                "          file_3.py",
5378                "    > dir_2",
5379            ]
5380        );
5381    }
5382
5383    #[gpui::test]
5384    async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
5385        init_test(cx);
5386
5387        let fs = FakeFs::new(cx.executor().clone());
5388        fs.as_fake().insert_tree("/root", json!({})).await;
5389        let project = Project::test(fs, ["/root".as_ref()], cx).await;
5390        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5391        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5392        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5393
5394        // Make a new buffer with no backing file
5395        workspace
5396            .update(cx, |workspace, cx| {
5397                Editor::new_file(workspace, &Default::default(), cx)
5398            })
5399            .unwrap();
5400
5401        cx.executor().run_until_parked();
5402
5403        // "Save as" the buffer, creating a new backing file for it
5404        let save_task = workspace
5405            .update(cx, |workspace, cx| {
5406                workspace.save_active_item(workspace::SaveIntent::Save, cx)
5407            })
5408            .unwrap();
5409
5410        cx.executor().run_until_parked();
5411        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new")));
5412        save_task.await.unwrap();
5413
5414        // Rename the file
5415        select_path(&panel, "root/new", cx);
5416        assert_eq!(
5417            visible_entries_as_strings(&panel, 0..10, cx),
5418            &["v root", "      new  <== selected"]
5419        );
5420        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
5421        panel.update(cx, |panel, cx| {
5422            panel
5423                .filename_editor
5424                .update(cx, |editor, cx| editor.set_text("newer", cx));
5425        });
5426        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
5427
5428        cx.executor().run_until_parked();
5429        assert_eq!(
5430            visible_entries_as_strings(&panel, 0..10, cx),
5431            &["v root", "      newer  <== selected"]
5432        );
5433
5434        workspace
5435            .update(cx, |workspace, cx| {
5436                workspace.save_active_item(workspace::SaveIntent::Save, cx)
5437            })
5438            .unwrap()
5439            .await
5440            .unwrap();
5441
5442        cx.executor().run_until_parked();
5443        // assert that saving the file doesn't restore "new"
5444        assert_eq!(
5445            visible_entries_as_strings(&panel, 0..10, cx),
5446            &["v root", "      newer  <== selected"]
5447        );
5448    }
5449
5450    #[gpui::test]
5451    async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
5452        init_test_with_editor(cx);
5453        let fs = FakeFs::new(cx.executor().clone());
5454        fs.insert_tree(
5455            "/project_root",
5456            json!({
5457                "dir_1": {
5458                    "nested_dir": {
5459                        "file_a.py": "# File contents",
5460                    }
5461                },
5462                "file_1.py": "# File contents",
5463            }),
5464        )
5465        .await;
5466
5467        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
5468        let worktree_id =
5469            cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
5470        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5471        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5472        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5473        cx.update(|cx| {
5474            panel.update(cx, |this, cx| {
5475                this.select_next(&Default::default(), cx);
5476                this.expand_selected_entry(&Default::default(), cx);
5477                this.expand_selected_entry(&Default::default(), cx);
5478                this.select_next(&Default::default(), cx);
5479                this.expand_selected_entry(&Default::default(), cx);
5480                this.select_next(&Default::default(), cx);
5481            })
5482        });
5483        assert_eq!(
5484            visible_entries_as_strings(&panel, 0..10, cx),
5485            &[
5486                "v project_root",
5487                "    v dir_1",
5488                "        v nested_dir",
5489                "              file_a.py  <== selected",
5490                "      file_1.py",
5491            ]
5492        );
5493        let modifiers_with_shift = gpui::Modifiers {
5494            shift: true,
5495            ..Default::default()
5496        };
5497        cx.simulate_modifiers_change(modifiers_with_shift);
5498        cx.update(|cx| {
5499            panel.update(cx, |this, cx| {
5500                this.select_next(&Default::default(), cx);
5501            })
5502        });
5503        assert_eq!(
5504            visible_entries_as_strings(&panel, 0..10, cx),
5505            &[
5506                "v project_root",
5507                "    v dir_1",
5508                "        v nested_dir",
5509                "              file_a.py",
5510                "      file_1.py  <== selected  <== marked",
5511            ]
5512        );
5513        cx.update(|cx| {
5514            panel.update(cx, |this, cx| {
5515                this.select_prev(&Default::default(), cx);
5516            })
5517        });
5518        assert_eq!(
5519            visible_entries_as_strings(&panel, 0..10, cx),
5520            &[
5521                "v project_root",
5522                "    v dir_1",
5523                "        v nested_dir",
5524                "              file_a.py  <== selected  <== marked",
5525                "      file_1.py  <== marked",
5526            ]
5527        );
5528        cx.update(|cx| {
5529            panel.update(cx, |this, cx| {
5530                let drag = DraggedSelection {
5531                    active_selection: this.selection.unwrap(),
5532                    marked_selections: Arc::new(this.marked_entries.clone()),
5533                };
5534                let target_entry = this
5535                    .project
5536                    .read(cx)
5537                    .entry_for_path(&(worktree_id, "").into(), cx)
5538                    .unwrap();
5539                this.drag_onto(&drag, target_entry.id, false, cx);
5540            });
5541        });
5542        cx.run_until_parked();
5543        assert_eq!(
5544            visible_entries_as_strings(&panel, 0..10, cx),
5545            &[
5546                "v project_root",
5547                "    v dir_1",
5548                "        v nested_dir",
5549                "      file_1.py  <== marked",
5550                "      file_a.py  <== selected  <== marked",
5551            ]
5552        );
5553        // ESC clears out all marks
5554        cx.update(|cx| {
5555            panel.update(cx, |this, cx| {
5556                this.cancel(&menu::Cancel, cx);
5557            })
5558        });
5559        assert_eq!(
5560            visible_entries_as_strings(&panel, 0..10, cx),
5561            &[
5562                "v project_root",
5563                "    v dir_1",
5564                "        v nested_dir",
5565                "      file_1.py",
5566                "      file_a.py  <== selected",
5567            ]
5568        );
5569        // ESC clears out all marks
5570        cx.update(|cx| {
5571            panel.update(cx, |this, cx| {
5572                this.select_prev(&SelectPrev, cx);
5573                this.select_next(&SelectNext, cx);
5574            })
5575        });
5576        assert_eq!(
5577            visible_entries_as_strings(&panel, 0..10, cx),
5578            &[
5579                "v project_root",
5580                "    v dir_1",
5581                "        v nested_dir",
5582                "      file_1.py  <== marked",
5583                "      file_a.py  <== selected  <== marked",
5584            ]
5585        );
5586        cx.simulate_modifiers_change(Default::default());
5587        cx.update(|cx| {
5588            panel.update(cx, |this, cx| {
5589                this.cut(&Cut, cx);
5590                this.select_prev(&SelectPrev, cx);
5591                this.select_prev(&SelectPrev, cx);
5592
5593                this.paste(&Paste, cx);
5594                // this.expand_selected_entry(&ExpandSelectedEntry, cx);
5595            })
5596        });
5597        cx.run_until_parked();
5598        assert_eq!(
5599            visible_entries_as_strings(&panel, 0..10, cx),
5600            &[
5601                "v project_root",
5602                "    v dir_1",
5603                "        v nested_dir",
5604                "              file_1.py  <== marked",
5605                "              file_a.py  <== selected  <== marked",
5606            ]
5607        );
5608        cx.simulate_modifiers_change(modifiers_with_shift);
5609        cx.update(|cx| {
5610            panel.update(cx, |this, cx| {
5611                this.expand_selected_entry(&Default::default(), cx);
5612                this.select_next(&SelectNext, cx);
5613                this.select_next(&SelectNext, cx);
5614            })
5615        });
5616        submit_deletion(&panel, cx);
5617        assert_eq!(
5618            visible_entries_as_strings(&panel, 0..10, cx),
5619            &["v project_root", "    v dir_1", "        v nested_dir",]
5620        );
5621    }
5622    #[gpui::test]
5623    async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
5624        init_test_with_editor(cx);
5625        cx.update(|cx| {
5626            cx.update_global::<SettingsStore, _>(|store, cx| {
5627                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5628                    worktree_settings.file_scan_exclusions = Some(Vec::new());
5629                });
5630                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5631                    project_panel_settings.auto_reveal_entries = Some(false)
5632                });
5633            })
5634        });
5635
5636        let fs = FakeFs::new(cx.background_executor.clone());
5637        fs.insert_tree(
5638            "/project_root",
5639            json!({
5640                ".git": {},
5641                ".gitignore": "**/gitignored_dir",
5642                "dir_1": {
5643                    "file_1.py": "# File 1_1 contents",
5644                    "file_2.py": "# File 1_2 contents",
5645                    "file_3.py": "# File 1_3 contents",
5646                    "gitignored_dir": {
5647                        "file_a.py": "# File contents",
5648                        "file_b.py": "# File contents",
5649                        "file_c.py": "# File contents",
5650                    },
5651                },
5652                "dir_2": {
5653                    "file_1.py": "# File 2_1 contents",
5654                    "file_2.py": "# File 2_2 contents",
5655                    "file_3.py": "# File 2_3 contents",
5656                }
5657            }),
5658        )
5659        .await;
5660
5661        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
5662        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5663        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5664        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5665
5666        assert_eq!(
5667            visible_entries_as_strings(&panel, 0..20, cx),
5668            &[
5669                "v project_root",
5670                "    > .git",
5671                "    > dir_1",
5672                "    > dir_2",
5673                "      .gitignore",
5674            ]
5675        );
5676
5677        let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
5678            .expect("dir 1 file is not ignored and should have an entry");
5679        let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
5680            .expect("dir 2 file is not ignored and should have an entry");
5681        let gitignored_dir_file =
5682            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
5683        assert_eq!(
5684            gitignored_dir_file, None,
5685            "File in the gitignored dir should not have an entry before its dir is toggled"
5686        );
5687
5688        toggle_expand_dir(&panel, "project_root/dir_1", cx);
5689        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
5690        cx.executor().run_until_parked();
5691        assert_eq!(
5692            visible_entries_as_strings(&panel, 0..20, cx),
5693            &[
5694                "v project_root",
5695                "    > .git",
5696                "    v dir_1",
5697                "        v gitignored_dir  <== selected",
5698                "              file_a.py",
5699                "              file_b.py",
5700                "              file_c.py",
5701                "          file_1.py",
5702                "          file_2.py",
5703                "          file_3.py",
5704                "    > dir_2",
5705                "      .gitignore",
5706            ],
5707            "Should show gitignored dir file list in the project panel"
5708        );
5709        let gitignored_dir_file =
5710            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
5711                .expect("after gitignored dir got opened, a file entry should be present");
5712
5713        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
5714        toggle_expand_dir(&panel, "project_root/dir_1", cx);
5715        assert_eq!(
5716            visible_entries_as_strings(&panel, 0..20, cx),
5717            &[
5718                "v project_root",
5719                "    > .git",
5720                "    > dir_1  <== selected",
5721                "    > dir_2",
5722                "      .gitignore",
5723            ],
5724            "Should hide all dir contents again and prepare for the auto reveal test"
5725        );
5726
5727        for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
5728            panel.update(cx, |panel, cx| {
5729                panel.project.update(cx, |_, cx| {
5730                    cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
5731                })
5732            });
5733            cx.run_until_parked();
5734            assert_eq!(
5735                visible_entries_as_strings(&panel, 0..20, cx),
5736                &[
5737                    "v project_root",
5738                    "    > .git",
5739                    "    > dir_1  <== selected",
5740                    "    > dir_2",
5741                    "      .gitignore",
5742                ],
5743                "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
5744            );
5745        }
5746
5747        cx.update(|cx| {
5748            cx.update_global::<SettingsStore, _>(|store, cx| {
5749                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5750                    project_panel_settings.auto_reveal_entries = Some(true)
5751                });
5752            })
5753        });
5754
5755        panel.update(cx, |panel, cx| {
5756            panel.project.update(cx, |_, cx| {
5757                cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
5758            })
5759        });
5760        cx.run_until_parked();
5761        assert_eq!(
5762            visible_entries_as_strings(&panel, 0..20, cx),
5763            &[
5764                "v project_root",
5765                "    > .git",
5766                "    v dir_1",
5767                "        > gitignored_dir",
5768                "          file_1.py  <== selected",
5769                "          file_2.py",
5770                "          file_3.py",
5771                "    > dir_2",
5772                "      .gitignore",
5773            ],
5774            "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
5775        );
5776
5777        panel.update(cx, |panel, cx| {
5778            panel.project.update(cx, |_, cx| {
5779                cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
5780            })
5781        });
5782        cx.run_until_parked();
5783        assert_eq!(
5784            visible_entries_as_strings(&panel, 0..20, cx),
5785            &[
5786                "v project_root",
5787                "    > .git",
5788                "    v dir_1",
5789                "        > gitignored_dir",
5790                "          file_1.py",
5791                "          file_2.py",
5792                "          file_3.py",
5793                "    v dir_2",
5794                "          file_1.py  <== selected",
5795                "          file_2.py",
5796                "          file_3.py",
5797                "      .gitignore",
5798            ],
5799            "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
5800        );
5801
5802        panel.update(cx, |panel, cx| {
5803            panel.project.update(cx, |_, cx| {
5804                cx.emit(project::Event::ActiveEntryChanged(Some(
5805                    gitignored_dir_file,
5806                )))
5807            })
5808        });
5809        cx.run_until_parked();
5810        assert_eq!(
5811            visible_entries_as_strings(&panel, 0..20, cx),
5812            &[
5813                "v project_root",
5814                "    > .git",
5815                "    v dir_1",
5816                "        > gitignored_dir",
5817                "          file_1.py",
5818                "          file_2.py",
5819                "          file_3.py",
5820                "    v dir_2",
5821                "          file_1.py  <== selected",
5822                "          file_2.py",
5823                "          file_3.py",
5824                "      .gitignore",
5825            ],
5826            "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
5827        );
5828
5829        panel.update(cx, |panel, cx| {
5830            panel.project.update(cx, |_, cx| {
5831                cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
5832            })
5833        });
5834        cx.run_until_parked();
5835        assert_eq!(
5836            visible_entries_as_strings(&panel, 0..20, cx),
5837            &[
5838                "v project_root",
5839                "    > .git",
5840                "    v dir_1",
5841                "        v gitignored_dir",
5842                "              file_a.py  <== selected",
5843                "              file_b.py",
5844                "              file_c.py",
5845                "          file_1.py",
5846                "          file_2.py",
5847                "          file_3.py",
5848                "    v dir_2",
5849                "          file_1.py",
5850                "          file_2.py",
5851                "          file_3.py",
5852                "      .gitignore",
5853            ],
5854            "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
5855        );
5856    }
5857
5858    #[gpui::test]
5859    async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
5860        init_test_with_editor(cx);
5861        cx.update(|cx| {
5862            cx.update_global::<SettingsStore, _>(|store, cx| {
5863                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5864                    worktree_settings.file_scan_exclusions = Some(Vec::new());
5865                });
5866                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5867                    project_panel_settings.auto_reveal_entries = Some(false)
5868                });
5869            })
5870        });
5871
5872        let fs = FakeFs::new(cx.background_executor.clone());
5873        fs.insert_tree(
5874            "/project_root",
5875            json!({
5876                ".git": {},
5877                ".gitignore": "**/gitignored_dir",
5878                "dir_1": {
5879                    "file_1.py": "# File 1_1 contents",
5880                    "file_2.py": "# File 1_2 contents",
5881                    "file_3.py": "# File 1_3 contents",
5882                    "gitignored_dir": {
5883                        "file_a.py": "# File contents",
5884                        "file_b.py": "# File contents",
5885                        "file_c.py": "# File contents",
5886                    },
5887                },
5888                "dir_2": {
5889                    "file_1.py": "# File 2_1 contents",
5890                    "file_2.py": "# File 2_2 contents",
5891                    "file_3.py": "# File 2_3 contents",
5892                }
5893            }),
5894        )
5895        .await;
5896
5897        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
5898        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5899        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5900        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5901
5902        assert_eq!(
5903            visible_entries_as_strings(&panel, 0..20, cx),
5904            &[
5905                "v project_root",
5906                "    > .git",
5907                "    > dir_1",
5908                "    > dir_2",
5909                "      .gitignore",
5910            ]
5911        );
5912
5913        let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
5914            .expect("dir 1 file is not ignored and should have an entry");
5915        let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
5916            .expect("dir 2 file is not ignored and should have an entry");
5917        let gitignored_dir_file =
5918            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
5919        assert_eq!(
5920            gitignored_dir_file, None,
5921            "File in the gitignored dir should not have an entry before its dir is toggled"
5922        );
5923
5924        toggle_expand_dir(&panel, "project_root/dir_1", cx);
5925        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
5926        cx.run_until_parked();
5927        assert_eq!(
5928            visible_entries_as_strings(&panel, 0..20, cx),
5929            &[
5930                "v project_root",
5931                "    > .git",
5932                "    v dir_1",
5933                "        v gitignored_dir  <== selected",
5934                "              file_a.py",
5935                "              file_b.py",
5936                "              file_c.py",
5937                "          file_1.py",
5938                "          file_2.py",
5939                "          file_3.py",
5940                "    > dir_2",
5941                "      .gitignore",
5942            ],
5943            "Should show gitignored dir file list in the project panel"
5944        );
5945        let gitignored_dir_file =
5946            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
5947                .expect("after gitignored dir got opened, a file entry should be present");
5948
5949        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
5950        toggle_expand_dir(&panel, "project_root/dir_1", cx);
5951        assert_eq!(
5952            visible_entries_as_strings(&panel, 0..20, cx),
5953            &[
5954                "v project_root",
5955                "    > .git",
5956                "    > dir_1  <== selected",
5957                "    > dir_2",
5958                "      .gitignore",
5959            ],
5960            "Should hide all dir contents again and prepare for the explicit reveal test"
5961        );
5962
5963        for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
5964            panel.update(cx, |panel, cx| {
5965                panel.project.update(cx, |_, cx| {
5966                    cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
5967                })
5968            });
5969            cx.run_until_parked();
5970            assert_eq!(
5971                visible_entries_as_strings(&panel, 0..20, cx),
5972                &[
5973                    "v project_root",
5974                    "    > .git",
5975                    "    > dir_1  <== selected",
5976                    "    > dir_2",
5977                    "      .gitignore",
5978                ],
5979                "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
5980            );
5981        }
5982
5983        panel.update(cx, |panel, cx| {
5984            panel.project.update(cx, |_, cx| {
5985                cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
5986            })
5987        });
5988        cx.run_until_parked();
5989        assert_eq!(
5990            visible_entries_as_strings(&panel, 0..20, cx),
5991            &[
5992                "v project_root",
5993                "    > .git",
5994                "    v dir_1",
5995                "        > gitignored_dir",
5996                "          file_1.py  <== selected",
5997                "          file_2.py",
5998                "          file_3.py",
5999                "    > dir_2",
6000                "      .gitignore",
6001            ],
6002            "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
6003        );
6004
6005        panel.update(cx, |panel, cx| {
6006            panel.project.update(cx, |_, cx| {
6007                cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
6008            })
6009        });
6010        cx.run_until_parked();
6011        assert_eq!(
6012            visible_entries_as_strings(&panel, 0..20, cx),
6013            &[
6014                "v project_root",
6015                "    > .git",
6016                "    v dir_1",
6017                "        > gitignored_dir",
6018                "          file_1.py",
6019                "          file_2.py",
6020                "          file_3.py",
6021                "    v dir_2",
6022                "          file_1.py  <== selected",
6023                "          file_2.py",
6024                "          file_3.py",
6025                "      .gitignore",
6026            ],
6027            "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
6028        );
6029
6030        panel.update(cx, |panel, cx| {
6031            panel.project.update(cx, |_, cx| {
6032                cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
6033            })
6034        });
6035        cx.run_until_parked();
6036        assert_eq!(
6037            visible_entries_as_strings(&panel, 0..20, cx),
6038            &[
6039                "v project_root",
6040                "    > .git",
6041                "    v dir_1",
6042                "        v gitignored_dir",
6043                "              file_a.py  <== selected",
6044                "              file_b.py",
6045                "              file_c.py",
6046                "          file_1.py",
6047                "          file_2.py",
6048                "          file_3.py",
6049                "    v dir_2",
6050                "          file_1.py",
6051                "          file_2.py",
6052                "          file_3.py",
6053                "      .gitignore",
6054            ],
6055            "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
6056        );
6057    }
6058
6059    #[gpui::test]
6060    async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
6061        init_test(cx);
6062        cx.update(|cx| {
6063            cx.update_global::<SettingsStore, _>(|store, cx| {
6064                store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
6065                    project_settings.file_scan_exclusions =
6066                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
6067                });
6068            });
6069        });
6070
6071        cx.update(|cx| {
6072            register_project_item::<TestProjectItemView>(cx);
6073        });
6074
6075        let fs = FakeFs::new(cx.executor().clone());
6076        fs.insert_tree(
6077            "/root1",
6078            json!({
6079                ".dockerignore": "",
6080                ".git": {
6081                    "HEAD": "",
6082                },
6083            }),
6084        )
6085        .await;
6086
6087        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
6088        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
6089        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6090        let panel = workspace
6091            .update(cx, |workspace, cx| {
6092                let panel = ProjectPanel::new(workspace, cx);
6093                workspace.add_panel(panel.clone(), cx);
6094                panel
6095            })
6096            .unwrap();
6097
6098        select_path(&panel, "root1", cx);
6099        assert_eq!(
6100            visible_entries_as_strings(&panel, 0..10, cx),
6101            &["v root1  <== selected", "      .dockerignore",]
6102        );
6103        workspace
6104            .update(cx, |workspace, cx| {
6105                assert!(
6106                    workspace.active_item(cx).is_none(),
6107                    "Should have no active items in the beginning"
6108                );
6109            })
6110            .unwrap();
6111
6112        let excluded_file_path = ".git/COMMIT_EDITMSG";
6113        let excluded_dir_path = "excluded_dir";
6114
6115        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
6116        panel.update(cx, |panel, cx| {
6117            assert!(panel.filename_editor.read(cx).is_focused(cx));
6118        });
6119        panel
6120            .update(cx, |panel, cx| {
6121                panel
6122                    .filename_editor
6123                    .update(cx, |editor, cx| editor.set_text(excluded_file_path, cx));
6124                panel.confirm_edit(cx).unwrap()
6125            })
6126            .await
6127            .unwrap();
6128
6129        assert_eq!(
6130            visible_entries_as_strings(&panel, 0..13, cx),
6131            &["v root1", "      .dockerignore"],
6132            "Excluded dir should not be shown after opening a file in it"
6133        );
6134        panel.update(cx, |panel, cx| {
6135            assert!(
6136                !panel.filename_editor.read(cx).is_focused(cx),
6137                "Should have closed the file name editor"
6138            );
6139        });
6140        workspace
6141            .update(cx, |workspace, cx| {
6142                let active_entry_path = workspace
6143                    .active_item(cx)
6144                    .expect("should have opened and activated the excluded item")
6145                    .act_as::<TestProjectItemView>(cx)
6146                    .expect(
6147                        "should have opened the corresponding project item for the excluded item",
6148                    )
6149                    .read(cx)
6150                    .path
6151                    .clone();
6152                assert_eq!(
6153                    active_entry_path.path.as_ref(),
6154                    Path::new(excluded_file_path),
6155                    "Should open the excluded file"
6156                );
6157
6158                assert!(
6159                    workspace.notification_ids().is_empty(),
6160                    "Should have no notifications after opening an excluded file"
6161                );
6162            })
6163            .unwrap();
6164        assert!(
6165            fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
6166            "Should have created the excluded file"
6167        );
6168
6169        select_path(&panel, "root1", cx);
6170        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
6171        panel.update(cx, |panel, cx| {
6172            assert!(panel.filename_editor.read(cx).is_focused(cx));
6173        });
6174        panel
6175            .update(cx, |panel, cx| {
6176                panel
6177                    .filename_editor
6178                    .update(cx, |editor, cx| editor.set_text(excluded_file_path, cx));
6179                panel.confirm_edit(cx).unwrap()
6180            })
6181            .await
6182            .unwrap();
6183
6184        assert_eq!(
6185            visible_entries_as_strings(&panel, 0..13, cx),
6186            &["v root1", "      .dockerignore"],
6187            "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
6188        );
6189        panel.update(cx, |panel, cx| {
6190            assert!(
6191                !panel.filename_editor.read(cx).is_focused(cx),
6192                "Should have closed the file name editor"
6193            );
6194        });
6195        workspace
6196            .update(cx, |workspace, cx| {
6197                let notifications = workspace.notification_ids();
6198                assert_eq!(
6199                    notifications.len(),
6200                    1,
6201                    "Should receive one notification with the error message"
6202                );
6203                workspace.dismiss_notification(notifications.first().unwrap(), cx);
6204                assert!(workspace.notification_ids().is_empty());
6205            })
6206            .unwrap();
6207
6208        select_path(&panel, "root1", cx);
6209        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
6210        panel.update(cx, |panel, cx| {
6211            assert!(panel.filename_editor.read(cx).is_focused(cx));
6212        });
6213        panel
6214            .update(cx, |panel, cx| {
6215                panel
6216                    .filename_editor
6217                    .update(cx, |editor, cx| editor.set_text(excluded_dir_path, cx));
6218                panel.confirm_edit(cx).unwrap()
6219            })
6220            .await
6221            .unwrap();
6222
6223        assert_eq!(
6224            visible_entries_as_strings(&panel, 0..13, cx),
6225            &["v root1", "      .dockerignore"],
6226            "Should not change the project panel after trying to create an excluded directory"
6227        );
6228        panel.update(cx, |panel, cx| {
6229            assert!(
6230                !panel.filename_editor.read(cx).is_focused(cx),
6231                "Should have closed the file name editor"
6232            );
6233        });
6234        workspace
6235            .update(cx, |workspace, cx| {
6236                let notifications = workspace.notification_ids();
6237                assert_eq!(
6238                    notifications.len(),
6239                    1,
6240                    "Should receive one notification explaining that no directory is actually shown"
6241                );
6242                workspace.dismiss_notification(notifications.first().unwrap(), cx);
6243                assert!(workspace.notification_ids().is_empty());
6244            })
6245            .unwrap();
6246        assert!(
6247            fs.is_dir(Path::new("/root1/excluded_dir")).await,
6248            "Should have created the excluded directory"
6249        );
6250    }
6251
6252    #[gpui::test]
6253    async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) {
6254        init_test_with_editor(cx);
6255
6256        let fs = FakeFs::new(cx.executor().clone());
6257        fs.insert_tree(
6258            "/src",
6259            json!({
6260                "test": {
6261                    "first.rs": "// First Rust file",
6262                    "second.rs": "// Second Rust file",
6263                    "third.rs": "// Third Rust file",
6264                }
6265            }),
6266        )
6267        .await;
6268
6269        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
6270        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
6271        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6272        let panel = workspace
6273            .update(cx, |workspace, cx| {
6274                let panel = ProjectPanel::new(workspace, cx);
6275                workspace.add_panel(panel.clone(), cx);
6276                panel
6277            })
6278            .unwrap();
6279
6280        select_path(&panel, "src/", cx);
6281        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
6282        cx.executor().run_until_parked();
6283        assert_eq!(
6284            visible_entries_as_strings(&panel, 0..10, cx),
6285            &[
6286                //
6287                "v src  <== selected",
6288                "    > test"
6289            ]
6290        );
6291        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
6292        panel.update(cx, |panel, cx| {
6293            assert!(panel.filename_editor.read(cx).is_focused(cx));
6294        });
6295        assert_eq!(
6296            visible_entries_as_strings(&panel, 0..10, cx),
6297            &[
6298                //
6299                "v src",
6300                "    > [EDITOR: '']  <== selected",
6301                "    > test"
6302            ]
6303        );
6304
6305        panel.update(cx, |panel, cx| panel.cancel(&menu::Cancel, cx));
6306        assert_eq!(
6307            visible_entries_as_strings(&panel, 0..10, cx),
6308            &[
6309                //
6310                "v src  <== selected",
6311                "    > test"
6312            ]
6313        );
6314    }
6315
6316    fn toggle_expand_dir(
6317        panel: &View<ProjectPanel>,
6318        path: impl AsRef<Path>,
6319        cx: &mut VisualTestContext,
6320    ) {
6321        let path = path.as_ref();
6322        panel.update(cx, |panel, cx| {
6323            for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
6324                let worktree = worktree.read(cx);
6325                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
6326                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
6327                    panel.toggle_expanded(entry_id, cx);
6328                    return;
6329                }
6330            }
6331            panic!("no worktree for path {:?}", path);
6332        });
6333    }
6334
6335    fn select_path(panel: &View<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
6336        let path = path.as_ref();
6337        panel.update(cx, |panel, cx| {
6338            for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
6339                let worktree = worktree.read(cx);
6340                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
6341                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
6342                    panel.selection = Some(crate::SelectedEntry {
6343                        worktree_id: worktree.id(),
6344                        entry_id,
6345                    });
6346                    return;
6347                }
6348            }
6349            panic!("no worktree for path {:?}", path);
6350        });
6351    }
6352
6353    fn find_project_entry(
6354        panel: &View<ProjectPanel>,
6355        path: impl AsRef<Path>,
6356        cx: &mut VisualTestContext,
6357    ) -> Option<ProjectEntryId> {
6358        let path = path.as_ref();
6359        panel.update(cx, |panel, cx| {
6360            for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
6361                let worktree = worktree.read(cx);
6362                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
6363                    return worktree.entry_for_path(relative_path).map(|entry| entry.id);
6364                }
6365            }
6366            panic!("no worktree for path {path:?}");
6367        })
6368    }
6369
6370    fn visible_entries_as_strings(
6371        panel: &View<ProjectPanel>,
6372        range: Range<usize>,
6373        cx: &mut VisualTestContext,
6374    ) -> Vec<String> {
6375        let mut result = Vec::new();
6376        let mut project_entries = HashSet::default();
6377        let mut has_editor = false;
6378
6379        panel.update(cx, |panel, cx| {
6380            panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
6381                if details.is_editing {
6382                    assert!(!has_editor, "duplicate editor entry");
6383                    has_editor = true;
6384                } else {
6385                    assert!(
6386                        project_entries.insert(project_entry),
6387                        "duplicate project entry {:?} {:?}",
6388                        project_entry,
6389                        details
6390                    );
6391                }
6392
6393                let indent = "    ".repeat(details.depth);
6394                let icon = if details.kind.is_dir() {
6395                    if details.is_expanded {
6396                        "v "
6397                    } else {
6398                        "> "
6399                    }
6400                } else {
6401                    "  "
6402                };
6403                let name = if details.is_editing {
6404                    format!("[EDITOR: '{}']", details.filename)
6405                } else if details.is_processing {
6406                    format!("[PROCESSING: '{}']", details.filename)
6407                } else {
6408                    details.filename.clone()
6409                };
6410                let selected = if details.is_selected {
6411                    "  <== selected"
6412                } else {
6413                    ""
6414                };
6415                let marked = if details.is_marked {
6416                    "  <== marked"
6417                } else {
6418                    ""
6419                };
6420
6421                result.push(format!("{indent}{icon}{name}{selected}{marked}"));
6422            });
6423        });
6424
6425        result
6426    }
6427
6428    fn init_test(cx: &mut TestAppContext) {
6429        cx.update(|cx| {
6430            let settings_store = SettingsStore::test(cx);
6431            cx.set_global(settings_store);
6432            init_settings(cx);
6433            theme::init(theme::LoadThemes::JustBase, cx);
6434            language::init(cx);
6435            editor::init_settings(cx);
6436            crate::init((), cx);
6437            workspace::init_settings(cx);
6438            client::init_settings(cx);
6439            Project::init_settings(cx);
6440
6441            cx.update_global::<SettingsStore, _>(|store, cx| {
6442                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
6443                    project_panel_settings.auto_fold_dirs = Some(false);
6444                });
6445                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
6446                    worktree_settings.file_scan_exclusions = Some(Vec::new());
6447                });
6448            });
6449        });
6450    }
6451
6452    fn init_test_with_editor(cx: &mut TestAppContext) {
6453        cx.update(|cx| {
6454            let app_state = AppState::test(cx);
6455            theme::init(theme::LoadThemes::JustBase, cx);
6456            init_settings(cx);
6457            language::init(cx);
6458            editor::init(cx);
6459            crate::init((), cx);
6460            workspace::init(app_state.clone(), cx);
6461            Project::init_settings(cx);
6462
6463            cx.update_global::<SettingsStore, _>(|store, cx| {
6464                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
6465                    project_panel_settings.auto_fold_dirs = Some(false);
6466                });
6467                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
6468                    worktree_settings.file_scan_exclusions = Some(Vec::new());
6469                });
6470            });
6471        });
6472    }
6473
6474    fn ensure_single_file_is_opened(
6475        window: &WindowHandle<Workspace>,
6476        expected_path: &str,
6477        cx: &mut TestAppContext,
6478    ) {
6479        window
6480            .update(cx, |workspace, cx| {
6481                let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
6482                assert_eq!(worktrees.len(), 1);
6483                let worktree_id = worktrees[0].read(cx).id();
6484
6485                let open_project_paths = workspace
6486                    .panes()
6487                    .iter()
6488                    .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
6489                    .collect::<Vec<_>>();
6490                assert_eq!(
6491                    open_project_paths,
6492                    vec![ProjectPath {
6493                        worktree_id,
6494                        path: Arc::from(Path::new(expected_path))
6495                    }],
6496                    "Should have opened file, selected in project panel"
6497                );
6498            })
6499            .unwrap();
6500    }
6501
6502    fn submit_deletion(panel: &View<ProjectPanel>, cx: &mut VisualTestContext) {
6503        assert!(
6504            !cx.has_pending_prompt(),
6505            "Should have no prompts before the deletion"
6506        );
6507        panel.update(cx, |panel, cx| {
6508            panel.delete(&Delete { skip_prompt: false }, cx)
6509        });
6510        assert!(
6511            cx.has_pending_prompt(),
6512            "Should have a prompt after the deletion"
6513        );
6514        cx.simulate_prompt_answer(0);
6515        assert!(
6516            !cx.has_pending_prompt(),
6517            "Should have no prompts after prompt was replied to"
6518        );
6519        cx.executor().run_until_parked();
6520    }
6521
6522    fn submit_deletion_skipping_prompt(panel: &View<ProjectPanel>, cx: &mut VisualTestContext) {
6523        assert!(
6524            !cx.has_pending_prompt(),
6525            "Should have no prompts before the deletion"
6526        );
6527        panel.update(cx, |panel, cx| {
6528            panel.delete(&Delete { skip_prompt: true }, cx)
6529        });
6530        assert!(!cx.has_pending_prompt(), "Should have received no prompts");
6531        cx.executor().run_until_parked();
6532    }
6533
6534    fn ensure_no_open_items_and_panes(
6535        workspace: &WindowHandle<Workspace>,
6536        cx: &mut VisualTestContext,
6537    ) {
6538        assert!(
6539            !cx.has_pending_prompt(),
6540            "Should have no prompts after deletion operation closes the file"
6541        );
6542        workspace
6543            .read_with(cx, |workspace, cx| {
6544                let open_project_paths = workspace
6545                    .panes()
6546                    .iter()
6547                    .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
6548                    .collect::<Vec<_>>();
6549                assert!(
6550                    open_project_paths.is_empty(),
6551                    "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
6552                );
6553            })
6554            .unwrap();
6555    }
6556
6557    struct TestProjectItemView {
6558        focus_handle: FocusHandle,
6559        path: ProjectPath,
6560    }
6561
6562    struct TestProjectItem {
6563        path: ProjectPath,
6564    }
6565
6566    impl project::Item for TestProjectItem {
6567        fn try_open(
6568            _project: &Model<Project>,
6569            path: &ProjectPath,
6570            cx: &mut AppContext,
6571        ) -> Option<Task<gpui::Result<Model<Self>>>> {
6572            let path = path.clone();
6573            Some(cx.spawn(|mut cx| async move { cx.new_model(|_| Self { path }) }))
6574        }
6575
6576        fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
6577            None
6578        }
6579
6580        fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
6581            Some(self.path.clone())
6582        }
6583    }
6584
6585    impl ProjectItem for TestProjectItemView {
6586        type Item = TestProjectItem;
6587
6588        fn for_project_item(
6589            _: Model<Project>,
6590            project_item: Model<Self::Item>,
6591            cx: &mut ViewContext<Self>,
6592        ) -> Self
6593        where
6594            Self: Sized,
6595        {
6596            Self {
6597                path: project_item.update(cx, |project_item, _| project_item.path.clone()),
6598                focus_handle: cx.focus_handle(),
6599            }
6600        }
6601    }
6602
6603    impl Item for TestProjectItemView {
6604        type Event = ();
6605    }
6606
6607    impl EventEmitter<()> for TestProjectItemView {}
6608
6609    impl FocusableView for TestProjectItemView {
6610        fn focus_handle(&self, _: &AppContext) -> FocusHandle {
6611            self.focus_handle.clone()
6612        }
6613    }
6614
6615    impl Render for TestProjectItemView {
6616        fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
6617            Empty
6618        }
6619    }
6620}