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