project_panel.rs

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