project_panel.rs

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