project_panel.rs

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