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