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                        .or_else(|| entry.path.file_name().map(Path::new).map(Arc::from))
1910                        .unwrap_or_else(|| entry.path.clone());
1911                    let depth = path.components().count();
1912                    (depth, path)
1913                };
1914                let width_estimate = item_width_estimate(
1915                    depth,
1916                    path.to_string_lossy().chars().count(),
1917                    entry.is_symlink,
1918                );
1919
1920                match max_width_item.as_mut() {
1921                    Some((id, worktree_id, width)) => {
1922                        if *width < width_estimate {
1923                            *id = entry.id;
1924                            *worktree_id = worktree.read(cx).id();
1925                            *width = width_estimate;
1926                        }
1927                    }
1928                    None => {
1929                        max_width_item = Some((entry.id, worktree.read(cx).id(), width_estimate))
1930                    }
1931                }
1932
1933                if expanded_dir_ids.binary_search(&entry.id).is_err()
1934                    && entry_iter.advance_to_sibling()
1935                {
1936                    continue;
1937                }
1938                entry_iter.advance();
1939            }
1940
1941            snapshot.propagate_git_statuses(&mut visible_worktree_entries);
1942            project::sort_worktree_entries(&mut visible_worktree_entries);
1943            self.visible_entries
1944                .push((worktree_id, visible_worktree_entries, OnceCell::new()));
1945        }
1946
1947        if let Some((project_entry_id, worktree_id, _)) = max_width_item {
1948            let mut visited_worktrees_length = 0;
1949            let index = self.visible_entries.iter().find_map(|(id, entries, _)| {
1950                if worktree_id == *id {
1951                    entries
1952                        .iter()
1953                        .position(|entry| entry.id == project_entry_id)
1954                } else {
1955                    visited_worktrees_length += entries.len();
1956                    None
1957                }
1958            });
1959            if let Some(index) = index {
1960                self.max_width_item_index = Some(visited_worktrees_length + index);
1961            }
1962        }
1963        if let Some((worktree_id, entry_id)) = new_selected_entry {
1964            self.selection = Some(SelectedEntry {
1965                worktree_id,
1966                entry_id,
1967            });
1968            if cx.modifiers().shift {
1969                self.marked_entries.insert(SelectedEntry {
1970                    worktree_id,
1971                    entry_id,
1972                });
1973            }
1974        }
1975    }
1976
1977    fn expand_entry(
1978        &mut self,
1979        worktree_id: WorktreeId,
1980        entry_id: ProjectEntryId,
1981        cx: &mut ViewContext<Self>,
1982    ) {
1983        self.project.update(cx, |project, cx| {
1984            if let Some((worktree, expanded_dir_ids)) = project
1985                .worktree_for_id(worktree_id, cx)
1986                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
1987            {
1988                project.expand_entry(worktree_id, entry_id, cx);
1989                let worktree = worktree.read(cx);
1990
1991                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
1992                    loop {
1993                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1994                            expanded_dir_ids.insert(ix, entry.id);
1995                        }
1996
1997                        if let Some(parent_entry) =
1998                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
1999                        {
2000                            entry = parent_entry;
2001                        } else {
2002                            break;
2003                        }
2004                    }
2005                }
2006            }
2007        });
2008    }
2009
2010    fn drop_external_files(
2011        &mut self,
2012        paths: &[PathBuf],
2013        entry_id: ProjectEntryId,
2014        cx: &mut ViewContext<Self>,
2015    ) {
2016        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
2017
2018        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
2019
2020        let Some((target_directory, worktree)) = maybe!({
2021            let worktree = self.project.read(cx).worktree_for_entry(entry_id, cx)?;
2022            let entry = worktree.read(cx).entry_for_id(entry_id)?;
2023            let path = worktree.read(cx).absolutize(&entry.path).ok()?;
2024            let target_directory = if path.is_dir() {
2025                path
2026            } else {
2027                path.parent()?.to_path_buf()
2028            };
2029            Some((target_directory, worktree))
2030        }) else {
2031            return;
2032        };
2033
2034        let mut paths_to_replace = Vec::new();
2035        for path in &paths {
2036            if let Some(name) = path.file_name() {
2037                let mut target_path = target_directory.clone();
2038                target_path.push(name);
2039                if target_path.exists() {
2040                    paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
2041                }
2042            }
2043        }
2044
2045        cx.spawn(|this, mut cx| {
2046            async move {
2047                for (filename, original_path) in &paths_to_replace {
2048                    let answer = cx
2049                        .prompt(
2050                            PromptLevel::Info,
2051                            format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(),
2052                            None,
2053                            &["Replace", "Cancel"],
2054                        )
2055                        .await?;
2056                    if answer == 1 {
2057                        if let Some(item_idx) = paths.iter().position(|p| p == original_path) {
2058                            paths.remove(item_idx);
2059                        }
2060                    }
2061                }
2062
2063                if paths.is_empty() {
2064                    return Ok(());
2065                }
2066
2067                let task = worktree.update(&mut cx, |worktree, cx| {
2068                    worktree.copy_external_entries(target_directory, paths, true, cx)
2069                })?;
2070
2071                let opened_entries = task.await?;
2072                this.update(&mut cx, |this, cx| {
2073                    if open_file_after_drop && !opened_entries.is_empty() {
2074                        this.open_entry(opened_entries[0], true, true, false, cx);
2075                    }
2076                })
2077            }
2078            .log_err()
2079        })
2080        .detach();
2081    }
2082
2083    fn drag_onto(
2084        &mut self,
2085        selections: &DraggedSelection,
2086        target_entry_id: ProjectEntryId,
2087        is_file: bool,
2088        cx: &mut ViewContext<Self>,
2089    ) {
2090        let should_copy = cx.modifiers().alt;
2091        if should_copy {
2092            let _ = maybe!({
2093                let project = self.project.read(cx);
2094                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
2095                let target_entry = target_worktree
2096                    .read(cx)
2097                    .entry_for_id(target_entry_id)?
2098                    .clone();
2099                for selection in selections.items() {
2100                    let new_path = self.create_paste_path(
2101                        selection,
2102                        (target_worktree.clone(), &target_entry),
2103                        cx,
2104                    )?;
2105                    self.project
2106                        .update(cx, |project, cx| {
2107                            project.copy_entry(selection.entry_id, None, new_path, cx)
2108                        })
2109                        .detach_and_log_err(cx)
2110                }
2111
2112                Some(())
2113            });
2114        } else {
2115            for selection in selections.items() {
2116                self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
2117            }
2118        }
2119    }
2120
2121    fn for_each_visible_entry(
2122        &self,
2123        range: Range<usize>,
2124        cx: &mut ViewContext<ProjectPanel>,
2125        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
2126    ) {
2127        let mut ix = 0;
2128        for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
2129            if ix >= range.end {
2130                return;
2131            }
2132
2133            if ix + visible_worktree_entries.len() <= range.start {
2134                ix += visible_worktree_entries.len();
2135                continue;
2136            }
2137
2138            let end_ix = range.end.min(ix + visible_worktree_entries.len());
2139            let (git_status_setting, show_file_icons, show_folder_icons) = {
2140                let settings = ProjectPanelSettings::get_global(cx);
2141                (
2142                    settings.git_status,
2143                    settings.file_icons,
2144                    settings.folder_icons,
2145                )
2146            };
2147            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
2148                let snapshot = worktree.read(cx).snapshot();
2149                let root_name = OsStr::new(snapshot.root_name());
2150                let expanded_entry_ids = self
2151                    .expanded_dir_ids
2152                    .get(&snapshot.id())
2153                    .map(Vec::as_slice)
2154                    .unwrap_or(&[]);
2155
2156                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
2157                let entries = entries_paths.get_or_init(|| {
2158                    visible_worktree_entries
2159                        .iter()
2160                        .map(|e| (e.path.clone()))
2161                        .collect()
2162                });
2163                for entry in visible_worktree_entries[entry_range].iter() {
2164                    let status = git_status_setting.then_some(entry.git_status).flatten();
2165                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
2166                    let icon = match entry.kind {
2167                        EntryKind::File => {
2168                            if show_file_icons {
2169                                FileIcons::get_icon(&entry.path, cx)
2170                            } else {
2171                                None
2172                            }
2173                        }
2174                        _ => {
2175                            if show_folder_icons {
2176                                FileIcons::get_folder_icon(is_expanded, cx)
2177                            } else {
2178                                FileIcons::get_chevron_icon(is_expanded, cx)
2179                            }
2180                        }
2181                    };
2182
2183                    let (depth, difference) =
2184                        ProjectPanel::calculate_depth_and_difference(entry, entries);
2185
2186                    let filename = match difference {
2187                        diff if diff > 1 => entry
2188                            .path
2189                            .iter()
2190                            .skip(entry.path.components().count() - diff)
2191                            .collect::<PathBuf>()
2192                            .to_str()
2193                            .unwrap_or_default()
2194                            .to_string(),
2195                        _ => entry
2196                            .path
2197                            .file_name()
2198                            .map(|name| name.to_string_lossy().into_owned())
2199                            .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
2200                    };
2201                    let selection = SelectedEntry {
2202                        worktree_id: snapshot.id(),
2203                        entry_id: entry.id,
2204                    };
2205                    let mut details = EntryDetails {
2206                        filename,
2207                        icon,
2208                        path: entry.path.clone(),
2209                        depth,
2210                        kind: entry.kind,
2211                        is_ignored: entry.is_ignored,
2212                        is_expanded,
2213                        is_selected: self.selection == Some(selection),
2214                        is_marked: self.marked_entries.contains(&selection),
2215                        is_editing: false,
2216                        is_processing: false,
2217                        is_cut: self
2218                            .clipboard
2219                            .as_ref()
2220                            .map_or(false, |e| e.is_cut() && e.items().contains(&selection)),
2221                        git_status: status,
2222                        is_private: entry.is_private,
2223                        worktree_id: *worktree_id,
2224                        canonical_path: entry.canonical_path.clone(),
2225                    };
2226
2227                    if let Some(edit_state) = &self.edit_state {
2228                        let is_edited_entry = if edit_state.is_new_entry {
2229                            entry.id == NEW_ENTRY_ID
2230                        } else {
2231                            entry.id == edit_state.entry_id
2232                                || self
2233                                    .ancestors
2234                                    .get(&entry.id)
2235                                    .is_some_and(|auto_folded_dirs| {
2236                                        auto_folded_dirs
2237                                            .ancestors
2238                                            .iter()
2239                                            .any(|entry_id| *entry_id == edit_state.entry_id)
2240                                    })
2241                        };
2242
2243                        if is_edited_entry {
2244                            if let Some(processing_filename) = &edit_state.processing_filename {
2245                                details.is_processing = true;
2246                                details.filename.clear();
2247                                details.filename.push_str(processing_filename);
2248                            } else {
2249                                if edit_state.is_new_entry {
2250                                    details.filename.clear();
2251                                }
2252                                details.is_editing = true;
2253                            }
2254                        }
2255                    }
2256
2257                    callback(entry.id, details, cx);
2258                }
2259            }
2260            ix = end_ix;
2261        }
2262    }
2263
2264    fn calculate_depth_and_difference(
2265        entry: &Entry,
2266        visible_worktree_entries: &HashSet<Arc<Path>>,
2267    ) -> (usize, usize) {
2268        let (depth, difference) = entry
2269            .path
2270            .ancestors()
2271            .skip(1) // Skip the entry itself
2272            .find_map(|ancestor| {
2273                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
2274                    let entry_path_components_count = entry.path.components().count();
2275                    let parent_path_components_count = parent_entry.components().count();
2276                    let difference = entry_path_components_count - parent_path_components_count;
2277                    let depth = parent_entry
2278                        .ancestors()
2279                        .skip(1)
2280                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
2281                        .count();
2282                    Some((depth + 1, difference))
2283                } else {
2284                    None
2285                }
2286            })
2287            .unwrap_or((0, 0));
2288
2289        (depth, difference)
2290    }
2291
2292    fn render_entry(
2293        &self,
2294        entry_id: ProjectEntryId,
2295        details: EntryDetails,
2296        cx: &mut ViewContext<Self>,
2297    ) -> Stateful<Div> {
2298        let kind = details.kind;
2299        let settings = ProjectPanelSettings::get_global(cx);
2300        let show_editor = details.is_editing && !details.is_processing;
2301        let selection = SelectedEntry {
2302            worktree_id: details.worktree_id,
2303            entry_id,
2304        };
2305        let is_marked = self.marked_entries.contains(&selection);
2306        let is_active = self
2307            .selection
2308            .map_or(false, |selection| selection.entry_id == entry_id);
2309        let width = self.size(cx);
2310        let filename_text_color =
2311            entry_git_aware_label_color(details.git_status, details.is_ignored, is_marked);
2312        let file_name = details.filename.clone();
2313        let mut icon = details.icon.clone();
2314        if settings.file_icons && show_editor && details.kind.is_file() {
2315            let filename = self.filename_editor.read(cx).text(cx);
2316            if filename.len() > 2 {
2317                icon = FileIcons::get_icon(Path::new(&filename), cx);
2318            }
2319        }
2320
2321        let canonical_path = details
2322            .canonical_path
2323            .as_ref()
2324            .map(|f| f.to_string_lossy().to_string());
2325        let path = details.path.clone();
2326
2327        let depth = details.depth;
2328        let worktree_id = details.worktree_id;
2329        let selections = Arc::new(self.marked_entries.clone());
2330
2331        let dragged_selection = DraggedSelection {
2332            active_selection: selection,
2333            marked_selections: selections,
2334        };
2335        div()
2336            .id(entry_id.to_proto() as usize)
2337            .on_drag_move::<ExternalPaths>(cx.listener(
2338                move |this, event: &DragMoveEvent<ExternalPaths>, cx| {
2339                    if event.bounds.contains(&event.event.position) {
2340                        if this.last_external_paths_drag_over_entry == Some(entry_id) {
2341                            return;
2342                        }
2343                        this.last_external_paths_drag_over_entry = Some(entry_id);
2344                        this.marked_entries.clear();
2345
2346                        let Some((worktree, path, entry)) = maybe!({
2347                            let worktree = this
2348                                .project
2349                                .read(cx)
2350                                .worktree_for_id(selection.worktree_id, cx)?;
2351                            let worktree = worktree.read(cx);
2352                            let abs_path = worktree.absolutize(&path).log_err()?;
2353                            let path = if abs_path.is_dir() {
2354                                path.as_ref()
2355                            } else {
2356                                path.parent()?
2357                            };
2358                            let entry = worktree.entry_for_path(path)?;
2359                            Some((worktree, path, entry))
2360                        }) else {
2361                            return;
2362                        };
2363
2364                        this.marked_entries.insert(SelectedEntry {
2365                            entry_id: entry.id,
2366                            worktree_id: worktree.id(),
2367                        });
2368
2369                        for entry in worktree.child_entries(path) {
2370                            this.marked_entries.insert(SelectedEntry {
2371                                entry_id: entry.id,
2372                                worktree_id: worktree.id(),
2373                            });
2374                        }
2375
2376                        cx.notify();
2377                    }
2378                },
2379            ))
2380            .on_drop(
2381                cx.listener(move |this, external_paths: &ExternalPaths, cx| {
2382                    this.last_external_paths_drag_over_entry = None;
2383                    this.marked_entries.clear();
2384                    this.drop_external_files(external_paths.paths(), entry_id, cx);
2385                    cx.stop_propagation();
2386                }),
2387            )
2388            .on_drag(dragged_selection, move |selection, cx| {
2389                cx.new_view(|_| DraggedProjectEntryView {
2390                    details: details.clone(),
2391                    width,
2392                    selection: selection.active_selection,
2393                    selections: selection.marked_selections.clone(),
2394                })
2395            })
2396            .drag_over::<DraggedSelection>(|style, _, cx| {
2397                style.bg(cx.theme().colors().drop_target_background)
2398            })
2399            .on_drop(cx.listener(move |this, selections: &DraggedSelection, cx| {
2400                this.drag_onto(selections, entry_id, kind.is_file(), cx);
2401            }))
2402            .child(
2403                ListItem::new(entry_id.to_proto() as usize)
2404                    .indent_level(depth)
2405                    .indent_step_size(px(settings.indent_size))
2406                    .selected(is_marked || is_active)
2407                    .when_some(canonical_path, |this, path| {
2408                        this.end_slot::<AnyElement>(
2409                            div()
2410                                .id("symlink_icon")
2411                                .pr_3()
2412                                .tooltip(move |cx| {
2413                                    Tooltip::with_meta(path.to_string(), None, "Symbolic Link", cx)
2414                                })
2415                                .child(
2416                                    Icon::new(IconName::ArrowUpRight)
2417                                        .size(IconSize::Indicator)
2418                                        .color(filename_text_color),
2419                                )
2420                                .into_any_element(),
2421                        )
2422                    })
2423                    .child(if let Some(icon) = &icon {
2424                        h_flex().child(Icon::from_path(icon.to_string()).color(filename_text_color))
2425                    } else {
2426                        h_flex()
2427                            .size(IconSize::default().rems())
2428                            .invisible()
2429                            .flex_none()
2430                    })
2431                    .child(
2432                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
2433                            h_flex().h_6().w_full().child(editor.clone())
2434                        } else {
2435                            h_flex().h_6().map(|this| {
2436                                if let Some(folded_ancestors) =
2437                                    is_active.then(|| self.ancestors.get(&entry_id)).flatten()
2438                                {
2439                                    let Some(part_to_highlight) = Path::new(&file_name)
2440                                        .ancestors()
2441                                        .nth(folded_ancestors.current_ancestor_depth)
2442                                    else {
2443                                        return this;
2444                                    };
2445
2446                                    let suffix = Path::new(&file_name)
2447                                        .strip_prefix(part_to_highlight)
2448                                        .ok()
2449                                        .filter(|suffix| !suffix.as_os_str().is_empty());
2450                                    let prefix = part_to_highlight
2451                                        .parent()
2452                                        .filter(|prefix| !prefix.as_os_str().is_empty());
2453                                    let Some(part_to_highlight) = part_to_highlight
2454                                        .file_name()
2455                                        .and_then(|name| name.to_str().map(String::from))
2456                                    else {
2457                                        return this;
2458                                    };
2459
2460                                    this.children(prefix.and_then(|prefix| {
2461                                        Some(
2462                                            h_flex()
2463                                                .child(
2464                                                    Label::new(prefix.to_str().map(String::from)?)
2465                                                        .single_line()
2466                                                        .color(filename_text_color),
2467                                                )
2468                                                .child(
2469                                                    Label::new(std::path::MAIN_SEPARATOR_STR)
2470                                                        .single_line()
2471                                                        .color(filename_text_color),
2472                                                ),
2473                                        )
2474                                    }))
2475                                    .child(
2476                                        Label::new(part_to_highlight)
2477                                            .single_line()
2478                                            .color(filename_text_color)
2479                                            .underline(true),
2480                                    )
2481                                    .children(
2482                                        suffix.and_then(|suffix| {
2483                                            Some(
2484                                                h_flex()
2485                                                    .child(
2486                                                        Label::new(std::path::MAIN_SEPARATOR_STR)
2487                                                            .single_line()
2488                                                            .color(filename_text_color),
2489                                                    )
2490                                                    .child(
2491                                                        Label::new(
2492                                                            suffix.to_str().map(String::from)?,
2493                                                        )
2494                                                        .single_line()
2495                                                        .color(filename_text_color),
2496                                                    ),
2497                                            )
2498                                        }),
2499                                    )
2500                                } else {
2501                                    this.child(
2502                                        Label::new(file_name)
2503                                            .single_line()
2504                                            .color(filename_text_color),
2505                                    )
2506                                }
2507                            })
2508                        }
2509                        .ml_1(),
2510                    )
2511                    .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
2512                        if event.down.button == MouseButton::Right || event.down.first_mouse {
2513                            return;
2514                        }
2515                        if !show_editor {
2516                            cx.stop_propagation();
2517
2518                            if let Some(selection) =
2519                                this.selection.filter(|_| event.down.modifiers.shift)
2520                            {
2521                                let current_selection = this.index_for_selection(selection);
2522                                let target_selection = this.index_for_selection(SelectedEntry {
2523                                    entry_id,
2524                                    worktree_id,
2525                                });
2526                                if let Some(((_, _, source_index), (_, _, target_index))) =
2527                                    current_selection.zip(target_selection)
2528                                {
2529                                    let range_start = source_index.min(target_index);
2530                                    let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
2531                                    let mut new_selections = BTreeSet::new();
2532                                    this.for_each_visible_entry(
2533                                        range_start..range_end,
2534                                        cx,
2535                                        |entry_id, details, _| {
2536                                            new_selections.insert(SelectedEntry {
2537                                                entry_id,
2538                                                worktree_id: details.worktree_id,
2539                                            });
2540                                        },
2541                                    );
2542
2543                                    this.marked_entries = this
2544                                        .marked_entries
2545                                        .union(&new_selections)
2546                                        .cloned()
2547                                        .collect();
2548
2549                                    this.selection = Some(SelectedEntry {
2550                                        entry_id,
2551                                        worktree_id,
2552                                    });
2553                                    // Ensure that the current entry is selected.
2554                                    this.marked_entries.insert(SelectedEntry {
2555                                        entry_id,
2556                                        worktree_id,
2557                                    });
2558                                }
2559                            } else if event.down.modifiers.secondary() {
2560                                if event.down.click_count > 1 {
2561                                    this.split_entry(entry_id, cx);
2562                                } else if !this.marked_entries.insert(selection) {
2563                                    this.marked_entries.remove(&selection);
2564                                }
2565                            } else if kind.is_dir() {
2566                                this.toggle_expanded(entry_id, cx);
2567                            } else {
2568                                let click_count = event.up.click_count;
2569                                this.open_entry(
2570                                    entry_id,
2571                                    cx.modifiers().secondary(),
2572                                    click_count > 1,
2573                                    click_count == 1,
2574                                    cx,
2575                                );
2576                            }
2577                        }
2578                    }))
2579                    .on_secondary_mouse_down(cx.listener(
2580                        move |this, event: &MouseDownEvent, cx| {
2581                            // Stop propagation to prevent the catch-all context menu for the project
2582                            // panel from being deployed.
2583                            cx.stop_propagation();
2584                            this.deploy_context_menu(event.position, entry_id, cx);
2585                        },
2586                    ))
2587                    .overflow_x(),
2588            )
2589            .border_1()
2590            .border_r_2()
2591            .rounded_none()
2592            .hover(|style| {
2593                if is_active {
2594                    style
2595                } else {
2596                    let hover_color = cx.theme().colors().ghost_element_hover;
2597                    style.bg(hover_color).border_color(hover_color)
2598                }
2599            })
2600            .when(is_marked || is_active, |this| {
2601                let colors = cx.theme().colors();
2602                this.when(is_marked, |this| this.bg(colors.ghost_element_selected))
2603                    .border_color(colors.ghost_element_selected)
2604            })
2605            .when(
2606                is_active && self.focus_handle.contains_focused(cx),
2607                |this| this.border_color(Color::Selected.color(cx)),
2608            )
2609    }
2610
2611    fn render_vertical_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
2612        if !Self::should_show_scrollbar(cx) {
2613            return None;
2614        }
2615        let scroll_handle = self.scroll_handle.0.borrow();
2616        let total_list_length = scroll_handle
2617            .last_item_size
2618            .filter(|_| {
2619                self.show_scrollbar || self.vertical_scrollbar_drag_thumb_offset.get().is_some()
2620            })?
2621            .contents
2622            .height
2623            .0 as f64;
2624        let current_offset = scroll_handle.base_handle.offset().y.0.min(0.).abs() as f64;
2625        let mut percentage = current_offset / total_list_length;
2626        let end_offset = (current_offset + scroll_handle.base_handle.bounds().size.height.0 as f64)
2627            / total_list_length;
2628        // Uniform scroll handle might briefly report an offset greater than the length of a list;
2629        // in such case we'll adjust the starting offset as well to keep the scrollbar thumb length stable.
2630        let overshoot = (end_offset - 1.).clamp(0., 1.);
2631        if overshoot > 0. {
2632            percentage -= overshoot;
2633        }
2634        const MINIMUM_SCROLLBAR_PERCENTAGE_HEIGHT: f64 = 0.005;
2635        if percentage + MINIMUM_SCROLLBAR_PERCENTAGE_HEIGHT > 1.0 || end_offset > total_list_length
2636        {
2637            return None;
2638        }
2639        if total_list_length < scroll_handle.base_handle.bounds().size.height.0 as f64 {
2640            return None;
2641        }
2642        let end_offset = end_offset.clamp(percentage + MINIMUM_SCROLLBAR_PERCENTAGE_HEIGHT, 1.);
2643        Some(
2644            div()
2645                .occlude()
2646                .id("project-panel-vertical-scroll")
2647                .on_mouse_move(cx.listener(|_, _, cx| {
2648                    cx.notify();
2649                    cx.stop_propagation()
2650                }))
2651                .on_hover(|_, cx| {
2652                    cx.stop_propagation();
2653                })
2654                .on_any_mouse_down(|_, cx| {
2655                    cx.stop_propagation();
2656                })
2657                .on_mouse_up(
2658                    MouseButton::Left,
2659                    cx.listener(|this, _, cx| {
2660                        if this.vertical_scrollbar_drag_thumb_offset.get().is_none()
2661                            && !this.focus_handle.contains_focused(cx)
2662                        {
2663                            this.hide_scrollbar(cx);
2664                            cx.notify();
2665                        }
2666
2667                        cx.stop_propagation();
2668                    }),
2669                )
2670                .on_scroll_wheel(cx.listener(|_, _, cx| {
2671                    cx.notify();
2672                }))
2673                .h_full()
2674                .absolute()
2675                .right_1()
2676                .top_1()
2677                .bottom_1()
2678                .w(px(12.))
2679                .cursor_default()
2680                .child(ProjectPanelScrollbar::vertical(
2681                    percentage as f32..end_offset as f32,
2682                    self.scroll_handle.clone(),
2683                    self.vertical_scrollbar_drag_thumb_offset.clone(),
2684                    cx.view().entity_id(),
2685                )),
2686        )
2687    }
2688
2689    fn render_horizontal_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
2690        if !Self::should_show_scrollbar(cx) {
2691            return None;
2692        }
2693        let scroll_handle = self.scroll_handle.0.borrow();
2694        let longest_item_width = scroll_handle
2695            .last_item_size
2696            .filter(|_| {
2697                self.show_scrollbar || self.horizontal_scrollbar_drag_thumb_offset.get().is_some()
2698            })
2699            .filter(|size| size.contents.width > size.item.width)?
2700            .contents
2701            .width
2702            .0 as f64;
2703        let current_offset = scroll_handle.base_handle.offset().x.0.min(0.).abs() as f64;
2704        let mut percentage = current_offset / longest_item_width;
2705        let end_offset = (current_offset + scroll_handle.base_handle.bounds().size.width.0 as f64)
2706            / longest_item_width;
2707        // Uniform scroll handle might briefly report an offset greater than the length of a list;
2708        // in such case we'll adjust the starting offset as well to keep the scrollbar thumb length stable.
2709        let overshoot = (end_offset - 1.).clamp(0., 1.);
2710        if overshoot > 0. {
2711            percentage -= overshoot;
2712        }
2713        const MINIMUM_SCROLLBAR_PERCENTAGE_WIDTH: f64 = 0.005;
2714        if percentage + MINIMUM_SCROLLBAR_PERCENTAGE_WIDTH > 1.0 || end_offset > longest_item_width
2715        {
2716            return None;
2717        }
2718        if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
2719            return None;
2720        }
2721        let end_offset = end_offset.clamp(percentage + MINIMUM_SCROLLBAR_PERCENTAGE_WIDTH, 1.);
2722        Some(
2723            div()
2724                .occlude()
2725                .id("project-panel-horizontal-scroll")
2726                .on_mouse_move(cx.listener(|_, _, cx| {
2727                    cx.notify();
2728                    cx.stop_propagation()
2729                }))
2730                .on_hover(|_, cx| {
2731                    cx.stop_propagation();
2732                })
2733                .on_any_mouse_down(|_, cx| {
2734                    cx.stop_propagation();
2735                })
2736                .on_mouse_up(
2737                    MouseButton::Left,
2738                    cx.listener(|this, _, cx| {
2739                        if this.horizontal_scrollbar_drag_thumb_offset.get().is_none()
2740                            && !this.focus_handle.contains_focused(cx)
2741                        {
2742                            this.hide_scrollbar(cx);
2743                            cx.notify();
2744                        }
2745
2746                        cx.stop_propagation();
2747                    }),
2748                )
2749                .on_scroll_wheel(cx.listener(|_, _, cx| {
2750                    cx.notify();
2751                }))
2752                .w_full()
2753                .absolute()
2754                .right_1()
2755                .left_1()
2756                .bottom_1()
2757                .h(px(12.))
2758                .cursor_default()
2759                .when(self.width.is_some(), |this| {
2760                    this.child(ProjectPanelScrollbar::horizontal(
2761                        percentage as f32..end_offset as f32,
2762                        self.scroll_handle.clone(),
2763                        self.horizontal_scrollbar_drag_thumb_offset.clone(),
2764                        cx.view().entity_id(),
2765                    ))
2766                }),
2767        )
2768    }
2769
2770    fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
2771        let mut dispatch_context = KeyContext::new_with_defaults();
2772        dispatch_context.add("ProjectPanel");
2773        dispatch_context.add("menu");
2774
2775        let identifier = if self.filename_editor.focus_handle(cx).is_focused(cx) {
2776            "editing"
2777        } else {
2778            "not_editing"
2779        };
2780
2781        dispatch_context.add(identifier);
2782        dispatch_context
2783    }
2784
2785    fn should_show_scrollbar(cx: &AppContext) -> bool {
2786        let show = ProjectPanelSettings::get_global(cx)
2787            .scrollbar
2788            .show
2789            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
2790        match show {
2791            ShowScrollbar::Auto => true,
2792            ShowScrollbar::System => true,
2793            ShowScrollbar::Always => true,
2794            ShowScrollbar::Never => false,
2795        }
2796    }
2797
2798    fn should_autohide_scrollbar(cx: &AppContext) -> bool {
2799        let show = ProjectPanelSettings::get_global(cx)
2800            .scrollbar
2801            .show
2802            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
2803        match show {
2804            ShowScrollbar::Auto => true,
2805            ShowScrollbar::System => cx
2806                .try_global::<ScrollbarAutoHide>()
2807                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
2808            ShowScrollbar::Always => false,
2809            ShowScrollbar::Never => true,
2810        }
2811    }
2812
2813    fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
2814        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
2815        if !Self::should_autohide_scrollbar(cx) {
2816            return;
2817        }
2818        self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
2819            cx.background_executor()
2820                .timer(SCROLLBAR_SHOW_INTERVAL)
2821                .await;
2822            panel
2823                .update(&mut cx, |panel, cx| {
2824                    panel.show_scrollbar = false;
2825                    cx.notify();
2826                })
2827                .log_err();
2828        }))
2829    }
2830
2831    fn reveal_entry(
2832        &mut self,
2833        project: Model<Project>,
2834        entry_id: ProjectEntryId,
2835        skip_ignored: bool,
2836        cx: &mut ViewContext<'_, Self>,
2837    ) {
2838        if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
2839            let worktree = worktree.read(cx);
2840            if skip_ignored
2841                && worktree
2842                    .entry_for_id(entry_id)
2843                    .map_or(true, |entry| entry.is_ignored)
2844            {
2845                return;
2846            }
2847
2848            let worktree_id = worktree.id();
2849            self.marked_entries.clear();
2850            self.expand_entry(worktree_id, entry_id, cx);
2851            self.update_visible_entries(Some((worktree_id, entry_id)), cx);
2852            self.autoscroll(cx);
2853            cx.notify();
2854        }
2855    }
2856}
2857
2858fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
2859    const ICON_SIZE_FACTOR: usize = 2;
2860    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
2861    if is_symlink {
2862        item_width += ICON_SIZE_FACTOR;
2863    }
2864    item_width
2865}
2866
2867impl Render for ProjectPanel {
2868    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
2869        let has_worktree = !self.visible_entries.is_empty();
2870        let project = self.project.read(cx);
2871
2872        if has_worktree {
2873            let item_count = self
2874                .visible_entries
2875                .iter()
2876                .map(|(_, worktree_entries, _)| worktree_entries.len())
2877                .sum();
2878
2879            h_flex()
2880                .id("project-panel")
2881                .group("project-panel")
2882                .size_full()
2883                .relative()
2884                .on_hover(cx.listener(|this, hovered, cx| {
2885                    if *hovered {
2886                        this.show_scrollbar = true;
2887                        this.hide_scrollbar_task.take();
2888                        cx.notify();
2889                    } else if !this.focus_handle.contains_focused(cx) {
2890                        this.hide_scrollbar(cx);
2891                    }
2892                }))
2893                .key_context(self.dispatch_context(cx))
2894                .on_action(cx.listener(Self::select_next))
2895                .on_action(cx.listener(Self::select_prev))
2896                .on_action(cx.listener(Self::select_first))
2897                .on_action(cx.listener(Self::select_last))
2898                .on_action(cx.listener(Self::select_parent))
2899                .on_action(cx.listener(Self::expand_selected_entry))
2900                .on_action(cx.listener(Self::collapse_selected_entry))
2901                .on_action(cx.listener(Self::collapse_all_entries))
2902                .on_action(cx.listener(Self::open))
2903                .on_action(cx.listener(Self::open_permanent))
2904                .on_action(cx.listener(Self::confirm))
2905                .on_action(cx.listener(Self::cancel))
2906                .on_action(cx.listener(Self::copy_path))
2907                .on_action(cx.listener(Self::copy_relative_path))
2908                .on_action(cx.listener(Self::new_search_in_directory))
2909                .on_action(cx.listener(Self::unfold_directory))
2910                .on_action(cx.listener(Self::fold_directory))
2911                .when(!project.is_read_only(), |el| {
2912                    el.on_action(cx.listener(Self::new_file))
2913                        .on_action(cx.listener(Self::new_directory))
2914                        .on_action(cx.listener(Self::rename))
2915                        .on_action(cx.listener(Self::delete))
2916                        .on_action(cx.listener(Self::trash))
2917                        .on_action(cx.listener(Self::cut))
2918                        .on_action(cx.listener(Self::copy))
2919                        .on_action(cx.listener(Self::paste))
2920                        .on_action(cx.listener(Self::duplicate))
2921                        .on_click(cx.listener(|this, event: &gpui::ClickEvent, cx| {
2922                            if event.up.click_count > 1 {
2923                                if let Some(entry_id) = this.last_worktree_root_id {
2924                                    let project = this.project.read(cx);
2925
2926                                    let worktree_id = if let Some(worktree) =
2927                                        project.worktree_for_entry(entry_id, cx)
2928                                    {
2929                                        worktree.read(cx).id()
2930                                    } else {
2931                                        return;
2932                                    };
2933
2934                                    this.selection = Some(SelectedEntry {
2935                                        worktree_id,
2936                                        entry_id,
2937                                    });
2938
2939                                    this.new_file(&NewFile, cx);
2940                                }
2941                            }
2942                        }))
2943                })
2944                .when(project.is_local(), |el| {
2945                    el.on_action(cx.listener(Self::reveal_in_finder))
2946                        .on_action(cx.listener(Self::open_system))
2947                        .on_action(cx.listener(Self::open_in_terminal))
2948                })
2949                .when(project.is_via_ssh(), |el| {
2950                    el.on_action(cx.listener(Self::open_in_terminal))
2951                })
2952                .on_mouse_down(
2953                    MouseButton::Right,
2954                    cx.listener(move |this, event: &MouseDownEvent, cx| {
2955                        // When deploying the context menu anywhere below the last project entry,
2956                        // act as if the user clicked the root of the last worktree.
2957                        if let Some(entry_id) = this.last_worktree_root_id {
2958                            this.deploy_context_menu(event.position, entry_id, cx);
2959                        }
2960                    }),
2961                )
2962                .track_focus(&self.focus_handle)
2963                .child(
2964                    uniform_list(cx.view().clone(), "entries", item_count, {
2965                        |this, range, cx| {
2966                            let mut items = Vec::with_capacity(range.end - range.start);
2967                            this.for_each_visible_entry(range, cx, |id, details, cx| {
2968                                items.push(this.render_entry(id, details, cx));
2969                            });
2970                            items
2971                        }
2972                    })
2973                    .size_full()
2974                    .with_sizing_behavior(ListSizingBehavior::Infer)
2975                    .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
2976                    .with_width_from_item(self.max_width_item_index)
2977                    .track_scroll(self.scroll_handle.clone()),
2978                )
2979                .children(self.render_vertical_scrollbar(cx))
2980                .children(self.render_horizontal_scrollbar(cx))
2981                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2982                    deferred(
2983                        anchored()
2984                            .position(*position)
2985                            .anchor(gpui::AnchorCorner::TopLeft)
2986                            .child(menu.clone()),
2987                    )
2988                    .with_priority(1)
2989                }))
2990        } else {
2991            v_flex()
2992                .id("empty-project_panel")
2993                .size_full()
2994                .p_4()
2995                .track_focus(&self.focus_handle)
2996                .child(
2997                    Button::new("open_project", "Open a project")
2998                        .full_width()
2999                        .key_binding(KeyBinding::for_action(&workspace::Open, cx))
3000                        .on_click(cx.listener(|this, _, cx| {
3001                            this.workspace
3002                                .update(cx, |workspace, cx| workspace.open(&workspace::Open, cx))
3003                                .log_err();
3004                        })),
3005                )
3006                .drag_over::<ExternalPaths>(|style, _, cx| {
3007                    style.bg(cx.theme().colors().drop_target_background)
3008                })
3009                .on_drop(
3010                    cx.listener(move |this, external_paths: &ExternalPaths, cx| {
3011                        this.last_external_paths_drag_over_entry = None;
3012                        this.marked_entries.clear();
3013                        if let Some(task) = this
3014                            .workspace
3015                            .update(cx, |workspace, cx| {
3016                                workspace.open_workspace_for_paths(
3017                                    true,
3018                                    external_paths.paths().to_owned(),
3019                                    cx,
3020                                )
3021                            })
3022                            .log_err()
3023                        {
3024                            task.detach_and_log_err(cx);
3025                        }
3026                        cx.stop_propagation();
3027                    }),
3028                )
3029        }
3030    }
3031}
3032
3033impl Render for DraggedProjectEntryView {
3034    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3035        let settings = ProjectPanelSettings::get_global(cx);
3036        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
3037        h_flex().font(ui_font).map(|this| {
3038            if self.selections.contains(&self.selection) {
3039                this.flex_shrink()
3040                    .p_1()
3041                    .items_end()
3042                    .rounded_md()
3043                    .child(self.selections.len().to_string())
3044            } else {
3045                this.bg(cx.theme().colors().background).w(self.width).child(
3046                    ListItem::new(self.selection.entry_id.to_proto() as usize)
3047                        .indent_level(self.details.depth)
3048                        .indent_step_size(px(settings.indent_size))
3049                        .child(if let Some(icon) = &self.details.icon {
3050                            div().child(Icon::from_path(icon.clone()))
3051                        } else {
3052                            div()
3053                        })
3054                        .child(Label::new(self.details.filename.clone())),
3055                )
3056            }
3057        })
3058    }
3059}
3060
3061impl EventEmitter<Event> for ProjectPanel {}
3062
3063impl EventEmitter<PanelEvent> for ProjectPanel {}
3064
3065impl Panel for ProjectPanel {
3066    fn position(&self, cx: &WindowContext) -> DockPosition {
3067        match ProjectPanelSettings::get_global(cx).dock {
3068            ProjectPanelDockPosition::Left => DockPosition::Left,
3069            ProjectPanelDockPosition::Right => DockPosition::Right,
3070        }
3071    }
3072
3073    fn position_is_valid(&self, position: DockPosition) -> bool {
3074        matches!(position, DockPosition::Left | DockPosition::Right)
3075    }
3076
3077    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3078        settings::update_settings_file::<ProjectPanelSettings>(
3079            self.fs.clone(),
3080            cx,
3081            move |settings, _| {
3082                let dock = match position {
3083                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
3084                    DockPosition::Right => ProjectPanelDockPosition::Right,
3085                };
3086                settings.dock = Some(dock);
3087            },
3088        );
3089    }
3090
3091    fn size(&self, cx: &WindowContext) -> Pixels {
3092        self.width
3093            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
3094    }
3095
3096    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
3097        self.width = size;
3098        self.serialize(cx);
3099        cx.notify();
3100    }
3101
3102    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
3103        ProjectPanelSettings::get_global(cx)
3104            .button
3105            .then_some(IconName::FileTree)
3106    }
3107
3108    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
3109        Some("Project Panel")
3110    }
3111
3112    fn toggle_action(&self) -> Box<dyn Action> {
3113        Box::new(ToggleFocus)
3114    }
3115
3116    fn persistent_name() -> &'static str {
3117        "Project Panel"
3118    }
3119
3120    fn starts_open(&self, cx: &WindowContext) -> bool {
3121        let project = &self.project.read(cx);
3122        project.dev_server_project_id().is_some()
3123            || project.visible_worktrees(cx).any(|tree| {
3124                tree.read(cx)
3125                    .root_entry()
3126                    .map_or(false, |entry| entry.is_dir())
3127            })
3128    }
3129}
3130
3131impl FocusableView for ProjectPanel {
3132    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
3133        self.focus_handle.clone()
3134    }
3135}
3136
3137impl ClipboardEntry {
3138    fn is_cut(&self) -> bool {
3139        matches!(self, Self::Cut { .. })
3140    }
3141
3142    fn items(&self) -> &BTreeSet<SelectedEntry> {
3143        match self {
3144            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
3145        }
3146    }
3147}
3148
3149#[cfg(test)]
3150mod tests {
3151    use super::*;
3152    use collections::HashSet;
3153    use gpui::{Empty, TestAppContext, View, VisualTestContext, WindowHandle};
3154    use pretty_assertions::assert_eq;
3155    use project::{FakeFs, WorktreeSettings};
3156    use serde_json::json;
3157    use settings::SettingsStore;
3158    use std::path::{Path, PathBuf};
3159    use ui::Context;
3160    use workspace::{
3161        item::{Item, ProjectItem},
3162        register_project_item, AppState,
3163    };
3164
3165    #[gpui::test]
3166    async fn test_visible_list(cx: &mut gpui::TestAppContext) {
3167        init_test(cx);
3168
3169        let fs = FakeFs::new(cx.executor().clone());
3170        fs.insert_tree(
3171            "/root1",
3172            json!({
3173                ".dockerignore": "",
3174                ".git": {
3175                    "HEAD": "",
3176                },
3177                "a": {
3178                    "0": { "q": "", "r": "", "s": "" },
3179                    "1": { "t": "", "u": "" },
3180                    "2": { "v": "", "w": "", "x": "", "y": "" },
3181                },
3182                "b": {
3183                    "3": { "Q": "" },
3184                    "4": { "R": "", "S": "", "T": "", "U": "" },
3185                },
3186                "C": {
3187                    "5": {},
3188                    "6": { "V": "", "W": "" },
3189                    "7": { "X": "" },
3190                    "8": { "Y": {}, "Z": "" }
3191                }
3192            }),
3193        )
3194        .await;
3195        fs.insert_tree(
3196            "/root2",
3197            json!({
3198                "d": {
3199                    "9": ""
3200                },
3201                "e": {}
3202            }),
3203        )
3204        .await;
3205
3206        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
3207        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3208        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3209        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3210        assert_eq!(
3211            visible_entries_as_strings(&panel, 0..50, cx),
3212            &[
3213                "v root1",
3214                "    > .git",
3215                "    > a",
3216                "    > b",
3217                "    > C",
3218                "      .dockerignore",
3219                "v root2",
3220                "    > d",
3221                "    > e",
3222            ]
3223        );
3224
3225        toggle_expand_dir(&panel, "root1/b", cx);
3226        assert_eq!(
3227            visible_entries_as_strings(&panel, 0..50, cx),
3228            &[
3229                "v root1",
3230                "    > .git",
3231                "    > a",
3232                "    v b  <== selected",
3233                "        > 3",
3234                "        > 4",
3235                "    > C",
3236                "      .dockerignore",
3237                "v root2",
3238                "    > d",
3239                "    > e",
3240            ]
3241        );
3242
3243        assert_eq!(
3244            visible_entries_as_strings(&panel, 6..9, cx),
3245            &[
3246                //
3247                "    > C",
3248                "      .dockerignore",
3249                "v root2",
3250            ]
3251        );
3252    }
3253
3254    #[gpui::test]
3255    async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
3256        init_test(cx);
3257        cx.update(|cx| {
3258            cx.update_global::<SettingsStore, _>(|store, cx| {
3259                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
3260                    worktree_settings.file_scan_exclusions =
3261                        Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
3262                });
3263            });
3264        });
3265
3266        let fs = FakeFs::new(cx.background_executor.clone());
3267        fs.insert_tree(
3268            "/root1",
3269            json!({
3270                ".dockerignore": "",
3271                ".git": {
3272                    "HEAD": "",
3273                },
3274                "a": {
3275                    "0": { "q": "", "r": "", "s": "" },
3276                    "1": { "t": "", "u": "" },
3277                    "2": { "v": "", "w": "", "x": "", "y": "" },
3278                },
3279                "b": {
3280                    "3": { "Q": "" },
3281                    "4": { "R": "", "S": "", "T": "", "U": "" },
3282                },
3283                "C": {
3284                    "5": {},
3285                    "6": { "V": "", "W": "" },
3286                    "7": { "X": "" },
3287                    "8": { "Y": {}, "Z": "" }
3288                }
3289            }),
3290        )
3291        .await;
3292        fs.insert_tree(
3293            "/root2",
3294            json!({
3295                "d": {
3296                    "4": ""
3297                },
3298                "e": {}
3299            }),
3300        )
3301        .await;
3302
3303        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
3304        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3305        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3306        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3307        assert_eq!(
3308            visible_entries_as_strings(&panel, 0..50, cx),
3309            &[
3310                "v root1",
3311                "    > a",
3312                "    > b",
3313                "    > C",
3314                "      .dockerignore",
3315                "v root2",
3316                "    > d",
3317                "    > e",
3318            ]
3319        );
3320
3321        toggle_expand_dir(&panel, "root1/b", cx);
3322        assert_eq!(
3323            visible_entries_as_strings(&panel, 0..50, cx),
3324            &[
3325                "v root1",
3326                "    > a",
3327                "    v b  <== selected",
3328                "        > 3",
3329                "    > C",
3330                "      .dockerignore",
3331                "v root2",
3332                "    > d",
3333                "    > e",
3334            ]
3335        );
3336
3337        toggle_expand_dir(&panel, "root2/d", cx);
3338        assert_eq!(
3339            visible_entries_as_strings(&panel, 0..50, cx),
3340            &[
3341                "v root1",
3342                "    > a",
3343                "    v b",
3344                "        > 3",
3345                "    > C",
3346                "      .dockerignore",
3347                "v root2",
3348                "    v d  <== selected",
3349                "    > e",
3350            ]
3351        );
3352
3353        toggle_expand_dir(&panel, "root2/e", cx);
3354        assert_eq!(
3355            visible_entries_as_strings(&panel, 0..50, cx),
3356            &[
3357                "v root1",
3358                "    > a",
3359                "    v b",
3360                "        > 3",
3361                "    > C",
3362                "      .dockerignore",
3363                "v root2",
3364                "    v d",
3365                "    v e  <== selected",
3366            ]
3367        );
3368    }
3369
3370    #[gpui::test]
3371    async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
3372        init_test(cx);
3373
3374        let fs = FakeFs::new(cx.executor().clone());
3375        fs.insert_tree(
3376            "/root1",
3377            json!({
3378                "dir_1": {
3379                    "nested_dir_1": {
3380                        "nested_dir_2": {
3381                            "nested_dir_3": {
3382                                "file_a.java": "// File contents",
3383                                "file_b.java": "// File contents",
3384                                "file_c.java": "// File contents",
3385                                "nested_dir_4": {
3386                                    "nested_dir_5": {
3387                                        "file_d.java": "// File contents",
3388                                    }
3389                                }
3390                            }
3391                        }
3392                    }
3393                }
3394            }),
3395        )
3396        .await;
3397        fs.insert_tree(
3398            "/root2",
3399            json!({
3400                "dir_2": {
3401                    "file_1.java": "// File contents",
3402                }
3403            }),
3404        )
3405        .await;
3406
3407        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
3408        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3409        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3410        cx.update(|cx| {
3411            let settings = *ProjectPanelSettings::get_global(cx);
3412            ProjectPanelSettings::override_global(
3413                ProjectPanelSettings {
3414                    auto_fold_dirs: true,
3415                    ..settings
3416                },
3417                cx,
3418            );
3419        });
3420        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
3421        assert_eq!(
3422            visible_entries_as_strings(&panel, 0..10, cx),
3423            &[
3424                "v root1",
3425                "    > dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
3426                "v root2",
3427                "    > dir_2",
3428            ]
3429        );
3430
3431        toggle_expand_dir(
3432            &panel,
3433            "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
3434            cx,
3435        );
3436        assert_eq!(
3437            visible_entries_as_strings(&panel, 0..10, cx),
3438            &[
3439                "v root1",
3440                "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3  <== selected",
3441                "        > nested_dir_4/nested_dir_5",
3442                "          file_a.java",
3443                "          file_b.java",
3444                "          file_c.java",
3445                "v root2",
3446                "    > dir_2",
3447            ]
3448        );
3449
3450        toggle_expand_dir(
3451            &panel,
3452            "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
3453            cx,
3454        );
3455        assert_eq!(
3456            visible_entries_as_strings(&panel, 0..10, cx),
3457            &[
3458                "v root1",
3459                "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
3460                "        v nested_dir_4/nested_dir_5  <== selected",
3461                "              file_d.java",
3462                "          file_a.java",
3463                "          file_b.java",
3464                "          file_c.java",
3465                "v root2",
3466                "    > dir_2",
3467            ]
3468        );
3469        toggle_expand_dir(&panel, "root2/dir_2", cx);
3470        assert_eq!(
3471            visible_entries_as_strings(&panel, 0..10, cx),
3472            &[
3473                "v root1",
3474                "    v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
3475                "        v nested_dir_4/nested_dir_5",
3476                "              file_d.java",
3477                "          file_a.java",
3478                "          file_b.java",
3479                "          file_c.java",
3480                "v root2",
3481                "    v dir_2  <== selected",
3482                "          file_1.java",
3483            ]
3484        );
3485    }
3486
3487    #[gpui::test(iterations = 30)]
3488    async fn test_editing_files(cx: &mut gpui::TestAppContext) {
3489        init_test(cx);
3490
3491        let fs = FakeFs::new(cx.executor().clone());
3492        fs.insert_tree(
3493            "/root1",
3494            json!({
3495                ".dockerignore": "",
3496                ".git": {
3497                    "HEAD": "",
3498                },
3499                "a": {
3500                    "0": { "q": "", "r": "", "s": "" },
3501                    "1": { "t": "", "u": "" },
3502                    "2": { "v": "", "w": "", "x": "", "y": "" },
3503                },
3504                "b": {
3505                    "3": { "Q": "" },
3506                    "4": { "R": "", "S": "", "T": "", "U": "" },
3507                },
3508                "C": {
3509                    "5": {},
3510                    "6": { "V": "", "W": "" },
3511                    "7": { "X": "" },
3512                    "8": { "Y": {}, "Z": "" }
3513                }
3514            }),
3515        )
3516        .await;
3517        fs.insert_tree(
3518            "/root2",
3519            json!({
3520                "d": {
3521                    "9": ""
3522                },
3523                "e": {}
3524            }),
3525        )
3526        .await;
3527
3528        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
3529        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3530        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3531        let panel = workspace
3532            .update(cx, |workspace, cx| {
3533                let panel = ProjectPanel::new(workspace, cx);
3534                workspace.add_panel(panel.clone(), cx);
3535                panel
3536            })
3537            .unwrap();
3538
3539        select_path(&panel, "root1", cx);
3540        assert_eq!(
3541            visible_entries_as_strings(&panel, 0..10, cx),
3542            &[
3543                "v root1  <== selected",
3544                "    > .git",
3545                "    > a",
3546                "    > b",
3547                "    > C",
3548                "      .dockerignore",
3549                "v root2",
3550                "    > d",
3551                "    > e",
3552            ]
3553        );
3554
3555        // Add a file with the root folder selected. The filename editor is placed
3556        // before the first file in the root folder.
3557        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
3558        panel.update(cx, |panel, cx| {
3559            assert!(panel.filename_editor.read(cx).is_focused(cx));
3560        });
3561        assert_eq!(
3562            visible_entries_as_strings(&panel, 0..10, cx),
3563            &[
3564                "v root1",
3565                "    > .git",
3566                "    > a",
3567                "    > b",
3568                "    > C",
3569                "      [EDITOR: '']  <== selected",
3570                "      .dockerignore",
3571                "v root2",
3572                "    > d",
3573                "    > e",
3574            ]
3575        );
3576
3577        let confirm = panel.update(cx, |panel, cx| {
3578            panel
3579                .filename_editor
3580                .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
3581            panel.confirm_edit(cx).unwrap()
3582        });
3583        assert_eq!(
3584            visible_entries_as_strings(&panel, 0..10, cx),
3585            &[
3586                "v root1",
3587                "    > .git",
3588                "    > a",
3589                "    > b",
3590                "    > C",
3591                "      [PROCESSING: 'the-new-filename']  <== selected",
3592                "      .dockerignore",
3593                "v root2",
3594                "    > d",
3595                "    > e",
3596            ]
3597        );
3598
3599        confirm.await.unwrap();
3600        assert_eq!(
3601            visible_entries_as_strings(&panel, 0..10, cx),
3602            &[
3603                "v root1",
3604                "    > .git",
3605                "    > a",
3606                "    > b",
3607                "    > C",
3608                "      .dockerignore",
3609                "      the-new-filename  <== selected  <== marked",
3610                "v root2",
3611                "    > d",
3612                "    > e",
3613            ]
3614        );
3615
3616        select_path(&panel, "root1/b", cx);
3617        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
3618        assert_eq!(
3619            visible_entries_as_strings(&panel, 0..10, cx),
3620            &[
3621                "v root1",
3622                "    > .git",
3623                "    > a",
3624                "    v b",
3625                "        > 3",
3626                "        > 4",
3627                "          [EDITOR: '']  <== selected",
3628                "    > C",
3629                "      .dockerignore",
3630                "      the-new-filename",
3631            ]
3632        );
3633
3634        panel
3635            .update(cx, |panel, cx| {
3636                panel
3637                    .filename_editor
3638                    .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx));
3639                panel.confirm_edit(cx).unwrap()
3640            })
3641            .await
3642            .unwrap();
3643        assert_eq!(
3644            visible_entries_as_strings(&panel, 0..10, cx),
3645            &[
3646                "v root1",
3647                "    > .git",
3648                "    > a",
3649                "    v b",
3650                "        > 3",
3651                "        > 4",
3652                "          another-filename.txt  <== selected  <== marked",
3653                "    > C",
3654                "      .dockerignore",
3655                "      the-new-filename",
3656            ]
3657        );
3658
3659        select_path(&panel, "root1/b/another-filename.txt", cx);
3660        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
3661        assert_eq!(
3662            visible_entries_as_strings(&panel, 0..10, cx),
3663            &[
3664                "v root1",
3665                "    > .git",
3666                "    > a",
3667                "    v b",
3668                "        > 3",
3669                "        > 4",
3670                "          [EDITOR: 'another-filename.txt']  <== selected  <== marked",
3671                "    > C",
3672                "      .dockerignore",
3673                "      the-new-filename",
3674            ]
3675        );
3676
3677        let confirm = panel.update(cx, |panel, cx| {
3678            panel.filename_editor.update(cx, |editor, cx| {
3679                let file_name_selections = editor.selections.all::<usize>(cx);
3680                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
3681                let file_name_selection = &file_name_selections[0];
3682                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
3683                assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
3684
3685                editor.set_text("a-different-filename.tar.gz", cx)
3686            });
3687            panel.confirm_edit(cx).unwrap()
3688        });
3689        assert_eq!(
3690            visible_entries_as_strings(&panel, 0..10, cx),
3691            &[
3692                "v root1",
3693                "    > .git",
3694                "    > a",
3695                "    v b",
3696                "        > 3",
3697                "        > 4",
3698                "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected  <== marked",
3699                "    > C",
3700                "      .dockerignore",
3701                "      the-new-filename",
3702            ]
3703        );
3704
3705        confirm.await.unwrap();
3706        assert_eq!(
3707            visible_entries_as_strings(&panel, 0..10, cx),
3708            &[
3709                "v root1",
3710                "    > .git",
3711                "    > a",
3712                "    v b",
3713                "        > 3",
3714                "        > 4",
3715                "          a-different-filename.tar.gz  <== selected",
3716                "    > C",
3717                "      .dockerignore",
3718                "      the-new-filename",
3719            ]
3720        );
3721
3722        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
3723        assert_eq!(
3724            visible_entries_as_strings(&panel, 0..10, cx),
3725            &[
3726                "v root1",
3727                "    > .git",
3728                "    > a",
3729                "    v b",
3730                "        > 3",
3731                "        > 4",
3732                "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
3733                "    > C",
3734                "      .dockerignore",
3735                "      the-new-filename",
3736            ]
3737        );
3738
3739        panel.update(cx, |panel, cx| {
3740            panel.filename_editor.update(cx, |editor, cx| {
3741                let file_name_selections = editor.selections.all::<usize>(cx);
3742                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
3743                let file_name_selection = &file_name_selections[0];
3744                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
3745                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..");
3746
3747            });
3748            panel.cancel(&menu::Cancel, cx)
3749        });
3750
3751        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
3752        assert_eq!(
3753            visible_entries_as_strings(&panel, 0..10, cx),
3754            &[
3755                "v root1",
3756                "    > .git",
3757                "    > a",
3758                "    v b",
3759                "        > 3",
3760                "        > 4",
3761                "        > [EDITOR: '']  <== selected",
3762                "          a-different-filename.tar.gz",
3763                "    > C",
3764                "      .dockerignore",
3765            ]
3766        );
3767
3768        let confirm = panel.update(cx, |panel, cx| {
3769            panel
3770                .filename_editor
3771                .update(cx, |editor, cx| editor.set_text("new-dir", cx));
3772            panel.confirm_edit(cx).unwrap()
3773        });
3774        panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
3775        assert_eq!(
3776            visible_entries_as_strings(&panel, 0..10, cx),
3777            &[
3778                "v root1",
3779                "    > .git",
3780                "    > a",
3781                "    v b",
3782                "        > 3",
3783                "        > 4",
3784                "        > [PROCESSING: 'new-dir']",
3785                "          a-different-filename.tar.gz  <== selected",
3786                "    > C",
3787                "      .dockerignore",
3788            ]
3789        );
3790
3791        confirm.await.unwrap();
3792        assert_eq!(
3793            visible_entries_as_strings(&panel, 0..10, cx),
3794            &[
3795                "v root1",
3796                "    > .git",
3797                "    > a",
3798                "    v b",
3799                "        > 3",
3800                "        > 4",
3801                "        > new-dir",
3802                "          a-different-filename.tar.gz  <== selected",
3803                "    > C",
3804                "      .dockerignore",
3805            ]
3806        );
3807
3808        panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx));
3809        assert_eq!(
3810            visible_entries_as_strings(&panel, 0..10, cx),
3811            &[
3812                "v root1",
3813                "    > .git",
3814                "    > a",
3815                "    v b",
3816                "        > 3",
3817                "        > 4",
3818                "        > new-dir",
3819                "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
3820                "    > C",
3821                "      .dockerignore",
3822            ]
3823        );
3824
3825        // Dismiss the rename editor when it loses focus.
3826        workspace.update(cx, |_, cx| cx.blur()).unwrap();
3827        assert_eq!(
3828            visible_entries_as_strings(&panel, 0..10, cx),
3829            &[
3830                "v root1",
3831                "    > .git",
3832                "    > a",
3833                "    v b",
3834                "        > 3",
3835                "        > 4",
3836                "        > new-dir",
3837                "          a-different-filename.tar.gz  <== selected",
3838                "    > C",
3839                "      .dockerignore",
3840            ]
3841        );
3842    }
3843
3844    #[gpui::test(iterations = 10)]
3845    async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
3846        init_test(cx);
3847
3848        let fs = FakeFs::new(cx.executor().clone());
3849        fs.insert_tree(
3850            "/root1",
3851            json!({
3852                ".dockerignore": "",
3853                ".git": {
3854                    "HEAD": "",
3855                },
3856                "a": {
3857                    "0": { "q": "", "r": "", "s": "" },
3858                    "1": { "t": "", "u": "" },
3859                    "2": { "v": "", "w": "", "x": "", "y": "" },
3860                },
3861                "b": {
3862                    "3": { "Q": "" },
3863                    "4": { "R": "", "S": "", "T": "", "U": "" },
3864                },
3865                "C": {
3866                    "5": {},
3867                    "6": { "V": "", "W": "" },
3868                    "7": { "X": "" },
3869                    "8": { "Y": {}, "Z": "" }
3870                }
3871            }),
3872        )
3873        .await;
3874        fs.insert_tree(
3875            "/root2",
3876            json!({
3877                "d": {
3878                    "9": ""
3879                },
3880                "e": {}
3881            }),
3882        )
3883        .await;
3884
3885        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
3886        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3887        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3888        let panel = workspace
3889            .update(cx, |workspace, cx| {
3890                let panel = ProjectPanel::new(workspace, cx);
3891                workspace.add_panel(panel.clone(), cx);
3892                panel
3893            })
3894            .unwrap();
3895
3896        select_path(&panel, "root1", cx);
3897        assert_eq!(
3898            visible_entries_as_strings(&panel, 0..10, cx),
3899            &[
3900                "v root1  <== selected",
3901                "    > .git",
3902                "    > a",
3903                "    > b",
3904                "    > C",
3905                "      .dockerignore",
3906                "v root2",
3907                "    > d",
3908                "    > e",
3909            ]
3910        );
3911
3912        // Add a file with the root folder selected. The filename editor is placed
3913        // before the first file in the root folder.
3914        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
3915        panel.update(cx, |panel, cx| {
3916            assert!(panel.filename_editor.read(cx).is_focused(cx));
3917        });
3918        assert_eq!(
3919            visible_entries_as_strings(&panel, 0..10, cx),
3920            &[
3921                "v root1",
3922                "    > .git",
3923                "    > a",
3924                "    > b",
3925                "    > C",
3926                "      [EDITOR: '']  <== selected",
3927                "      .dockerignore",
3928                "v root2",
3929                "    > d",
3930                "    > e",
3931            ]
3932        );
3933
3934        let confirm = panel.update(cx, |panel, cx| {
3935            panel.filename_editor.update(cx, |editor, cx| {
3936                editor.set_text("/bdir1/dir2/the-new-filename", cx)
3937            });
3938            panel.confirm_edit(cx).unwrap()
3939        });
3940
3941        assert_eq!(
3942            visible_entries_as_strings(&panel, 0..10, cx),
3943            &[
3944                "v root1",
3945                "    > .git",
3946                "    > a",
3947                "    > b",
3948                "    > C",
3949                "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
3950                "      .dockerignore",
3951                "v root2",
3952                "    > d",
3953                "    > e",
3954            ]
3955        );
3956
3957        confirm.await.unwrap();
3958        assert_eq!(
3959            visible_entries_as_strings(&panel, 0..13, cx),
3960            &[
3961                "v root1",
3962                "    > .git",
3963                "    > a",
3964                "    > b",
3965                "    v bdir1",
3966                "        v dir2",
3967                "              the-new-filename  <== selected  <== marked",
3968                "    > C",
3969                "      .dockerignore",
3970                "v root2",
3971                "    > d",
3972                "    > e",
3973            ]
3974        );
3975    }
3976
3977    #[gpui::test]
3978    async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
3979        init_test(cx);
3980
3981        let fs = FakeFs::new(cx.executor().clone());
3982        fs.insert_tree(
3983            "/root1",
3984            json!({
3985                ".dockerignore": "",
3986                ".git": {
3987                    "HEAD": "",
3988                },
3989            }),
3990        )
3991        .await;
3992
3993        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
3994        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3995        let cx = &mut VisualTestContext::from_window(*workspace, cx);
3996        let panel = workspace
3997            .update(cx, |workspace, cx| {
3998                let panel = ProjectPanel::new(workspace, cx);
3999                workspace.add_panel(panel.clone(), cx);
4000                panel
4001            })
4002            .unwrap();
4003
4004        select_path(&panel, "root1", cx);
4005        assert_eq!(
4006            visible_entries_as_strings(&panel, 0..10, cx),
4007            &["v root1  <== selected", "    > .git", "      .dockerignore",]
4008        );
4009
4010        // Add a file with the root folder selected. The filename editor is placed
4011        // before the first file in the root folder.
4012        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
4013        panel.update(cx, |panel, cx| {
4014            assert!(panel.filename_editor.read(cx).is_focused(cx));
4015        });
4016        assert_eq!(
4017            visible_entries_as_strings(&panel, 0..10, cx),
4018            &[
4019                "v root1",
4020                "    > .git",
4021                "      [EDITOR: '']  <== selected",
4022                "      .dockerignore",
4023            ]
4024        );
4025
4026        let confirm = panel.update(cx, |panel, cx| {
4027            panel
4028                .filename_editor
4029                .update(cx, |editor, cx| editor.set_text("/new_dir/", cx));
4030            panel.confirm_edit(cx).unwrap()
4031        });
4032
4033        assert_eq!(
4034            visible_entries_as_strings(&panel, 0..10, cx),
4035            &[
4036                "v root1",
4037                "    > .git",
4038                "      [PROCESSING: '/new_dir/']  <== selected",
4039                "      .dockerignore",
4040            ]
4041        );
4042
4043        confirm.await.unwrap();
4044        assert_eq!(
4045            visible_entries_as_strings(&panel, 0..13, cx),
4046            &[
4047                "v root1",
4048                "    > .git",
4049                "    v new_dir  <== selected",
4050                "      .dockerignore",
4051            ]
4052        );
4053    }
4054
4055    #[gpui::test]
4056    async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
4057        init_test(cx);
4058
4059        let fs = FakeFs::new(cx.executor().clone());
4060        fs.insert_tree(
4061            "/root1",
4062            json!({
4063                "one.two.txt": "",
4064                "one.txt": ""
4065            }),
4066        )
4067        .await;
4068
4069        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
4070        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4071        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4072        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4073
4074        panel.update(cx, |panel, cx| {
4075            panel.select_next(&Default::default(), cx);
4076            panel.select_next(&Default::default(), cx);
4077        });
4078
4079        assert_eq!(
4080            visible_entries_as_strings(&panel, 0..50, cx),
4081            &[
4082                //
4083                "v root1",
4084                "      one.txt  <== selected",
4085                "      one.two.txt",
4086            ]
4087        );
4088
4089        // Regression test - file name is created correctly when
4090        // the copied file's name contains multiple dots.
4091        panel.update(cx, |panel, cx| {
4092            panel.copy(&Default::default(), cx);
4093            panel.paste(&Default::default(), cx);
4094        });
4095        cx.executor().run_until_parked();
4096
4097        assert_eq!(
4098            visible_entries_as_strings(&panel, 0..50, cx),
4099            &[
4100                //
4101                "v root1",
4102                "      one.txt",
4103                "      one copy.txt  <== selected",
4104                "      one.two.txt",
4105            ]
4106        );
4107
4108        panel.update(cx, |panel, cx| {
4109            panel.paste(&Default::default(), cx);
4110        });
4111        cx.executor().run_until_parked();
4112
4113        assert_eq!(
4114            visible_entries_as_strings(&panel, 0..50, cx),
4115            &[
4116                //
4117                "v root1",
4118                "      one.txt",
4119                "      one copy.txt",
4120                "      one copy 1.txt  <== selected",
4121                "      one.two.txt",
4122            ]
4123        );
4124    }
4125
4126    #[gpui::test]
4127    async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
4128        init_test(cx);
4129
4130        let fs = FakeFs::new(cx.executor().clone());
4131        fs.insert_tree(
4132            "/root1",
4133            json!({
4134                "one.txt": "",
4135                "two.txt": "",
4136                "three.txt": "",
4137                "a": {
4138                    "0": { "q": "", "r": "", "s": "" },
4139                    "1": { "t": "", "u": "" },
4140                    "2": { "v": "", "w": "", "x": "", "y": "" },
4141                },
4142            }),
4143        )
4144        .await;
4145
4146        fs.insert_tree(
4147            "/root2",
4148            json!({
4149                "one.txt": "",
4150                "two.txt": "",
4151                "four.txt": "",
4152                "b": {
4153                    "3": { "Q": "" },
4154                    "4": { "R": "", "S": "", "T": "", "U": "" },
4155                },
4156            }),
4157        )
4158        .await;
4159
4160        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4161        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4162        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4163        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4164
4165        select_path(&panel, "root1/three.txt", cx);
4166        panel.update(cx, |panel, cx| {
4167            panel.cut(&Default::default(), cx);
4168        });
4169
4170        select_path(&panel, "root2/one.txt", cx);
4171        panel.update(cx, |panel, cx| {
4172            panel.select_next(&Default::default(), cx);
4173            panel.paste(&Default::default(), cx);
4174        });
4175        cx.executor().run_until_parked();
4176        assert_eq!(
4177            visible_entries_as_strings(&panel, 0..50, cx),
4178            &[
4179                //
4180                "v root1",
4181                "    > a",
4182                "      one.txt",
4183                "      two.txt",
4184                "v root2",
4185                "    > b",
4186                "      four.txt",
4187                "      one.txt",
4188                "      three.txt  <== selected",
4189                "      two.txt",
4190            ]
4191        );
4192
4193        select_path(&panel, "root1/a", cx);
4194        panel.update(cx, |panel, cx| {
4195            panel.cut(&Default::default(), cx);
4196        });
4197        select_path(&panel, "root2/two.txt", cx);
4198        panel.update(cx, |panel, cx| {
4199            panel.select_next(&Default::default(), cx);
4200            panel.paste(&Default::default(), cx);
4201        });
4202
4203        cx.executor().run_until_parked();
4204        assert_eq!(
4205            visible_entries_as_strings(&panel, 0..50, cx),
4206            &[
4207                //
4208                "v root1",
4209                "      one.txt",
4210                "      two.txt",
4211                "v root2",
4212                "    > a  <== selected",
4213                "    > b",
4214                "      four.txt",
4215                "      one.txt",
4216                "      three.txt",
4217                "      two.txt",
4218            ]
4219        );
4220    }
4221
4222    #[gpui::test]
4223    async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
4224        init_test(cx);
4225
4226        let fs = FakeFs::new(cx.executor().clone());
4227        fs.insert_tree(
4228            "/root1",
4229            json!({
4230                "one.txt": "",
4231                "two.txt": "",
4232                "three.txt": "",
4233                "a": {
4234                    "0": { "q": "", "r": "", "s": "" },
4235                    "1": { "t": "", "u": "" },
4236                    "2": { "v": "", "w": "", "x": "", "y": "" },
4237                },
4238            }),
4239        )
4240        .await;
4241
4242        fs.insert_tree(
4243            "/root2",
4244            json!({
4245                "one.txt": "",
4246                "two.txt": "",
4247                "four.txt": "",
4248                "b": {
4249                    "3": { "Q": "" },
4250                    "4": { "R": "", "S": "", "T": "", "U": "" },
4251                },
4252            }),
4253        )
4254        .await;
4255
4256        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
4257        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4258        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4259        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4260
4261        select_path(&panel, "root1/three.txt", cx);
4262        panel.update(cx, |panel, cx| {
4263            panel.copy(&Default::default(), cx);
4264        });
4265
4266        select_path(&panel, "root2/one.txt", cx);
4267        panel.update(cx, |panel, cx| {
4268            panel.select_next(&Default::default(), cx);
4269            panel.paste(&Default::default(), cx);
4270        });
4271        cx.executor().run_until_parked();
4272        assert_eq!(
4273            visible_entries_as_strings(&panel, 0..50, cx),
4274            &[
4275                //
4276                "v root1",
4277                "    > a",
4278                "      one.txt",
4279                "      three.txt",
4280                "      two.txt",
4281                "v root2",
4282                "    > b",
4283                "      four.txt",
4284                "      one.txt",
4285                "      three.txt  <== selected",
4286                "      two.txt",
4287            ]
4288        );
4289
4290        select_path(&panel, "root1/three.txt", cx);
4291        panel.update(cx, |panel, cx| {
4292            panel.copy(&Default::default(), cx);
4293        });
4294        select_path(&panel, "root2/two.txt", cx);
4295        panel.update(cx, |panel, cx| {
4296            panel.select_next(&Default::default(), cx);
4297            panel.paste(&Default::default(), cx);
4298        });
4299
4300        cx.executor().run_until_parked();
4301        assert_eq!(
4302            visible_entries_as_strings(&panel, 0..50, cx),
4303            &[
4304                //
4305                "v root1",
4306                "    > a",
4307                "      one.txt",
4308                "      three.txt",
4309                "      two.txt",
4310                "v root2",
4311                "    > b",
4312                "      four.txt",
4313                "      one.txt",
4314                "      three.txt",
4315                "      three copy.txt  <== selected",
4316                "      two.txt",
4317            ]
4318        );
4319
4320        select_path(&panel, "root1/a", cx);
4321        panel.update(cx, |panel, cx| {
4322            panel.copy(&Default::default(), cx);
4323        });
4324        select_path(&panel, "root2/two.txt", cx);
4325        panel.update(cx, |panel, cx| {
4326            panel.select_next(&Default::default(), cx);
4327            panel.paste(&Default::default(), cx);
4328        });
4329
4330        cx.executor().run_until_parked();
4331        assert_eq!(
4332            visible_entries_as_strings(&panel, 0..50, cx),
4333            &[
4334                //
4335                "v root1",
4336                "    > a",
4337                "      one.txt",
4338                "      three.txt",
4339                "      two.txt",
4340                "v root2",
4341                "    > a  <== selected",
4342                "    > b",
4343                "      four.txt",
4344                "      one.txt",
4345                "      three.txt",
4346                "      three copy.txt",
4347                "      two.txt",
4348            ]
4349        );
4350    }
4351
4352    #[gpui::test]
4353    async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) {
4354        init_test(cx);
4355
4356        let fs = FakeFs::new(cx.executor().clone());
4357        fs.insert_tree(
4358            "/root",
4359            json!({
4360                "a": {
4361                    "one.txt": "",
4362                    "two.txt": "",
4363                    "inner_dir": {
4364                        "three.txt": "",
4365                        "four.txt": "",
4366                    }
4367                },
4368                "b": {}
4369            }),
4370        )
4371        .await;
4372
4373        let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
4374        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4375        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4376        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4377
4378        select_path(&panel, "root/a", cx);
4379        panel.update(cx, |panel, cx| {
4380            panel.copy(&Default::default(), cx);
4381            panel.select_next(&Default::default(), cx);
4382            panel.paste(&Default::default(), cx);
4383        });
4384        cx.executor().run_until_parked();
4385
4386        let pasted_dir = find_project_entry(&panel, "root/b/a", cx);
4387        assert_ne!(pasted_dir, None, "Pasted directory should have an entry");
4388
4389        let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx);
4390        assert_ne!(
4391            pasted_dir_file, None,
4392            "Pasted directory file should have an entry"
4393        );
4394
4395        let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx);
4396        assert_ne!(
4397            pasted_dir_inner_dir, None,
4398            "Directories inside pasted directory should have an entry"
4399        );
4400
4401        toggle_expand_dir(&panel, "root/b/a", cx);
4402        toggle_expand_dir(&panel, "root/b/a/inner_dir", cx);
4403
4404        assert_eq!(
4405            visible_entries_as_strings(&panel, 0..50, cx),
4406            &[
4407                //
4408                "v root",
4409                "    > a",
4410                "    v b",
4411                "        v a",
4412                "            v inner_dir  <== selected",
4413                "                  four.txt",
4414                "                  three.txt",
4415                "              one.txt",
4416                "              two.txt",
4417            ]
4418        );
4419
4420        select_path(&panel, "root", cx);
4421        panel.update(cx, |panel, cx| panel.paste(&Default::default(), cx));
4422        cx.executor().run_until_parked();
4423        panel.update(cx, |panel, cx| panel.paste(&Default::default(), cx));
4424        cx.executor().run_until_parked();
4425        assert_eq!(
4426            visible_entries_as_strings(&panel, 0..50, cx),
4427            &[
4428                //
4429                "v root",
4430                "    > a",
4431                "    v a copy",
4432                "        > a  <== selected",
4433                "        > inner_dir",
4434                "          one.txt",
4435                "          two.txt",
4436                "    v b",
4437                "        v a",
4438                "            v inner_dir",
4439                "                  four.txt",
4440                "                  three.txt",
4441                "              one.txt",
4442                "              two.txt"
4443            ]
4444        );
4445    }
4446
4447    #[gpui::test]
4448    async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
4449        init_test_with_editor(cx);
4450
4451        let fs = FakeFs::new(cx.executor().clone());
4452        fs.insert_tree(
4453            "/src",
4454            json!({
4455                "test": {
4456                    "first.rs": "// First Rust file",
4457                    "second.rs": "// Second Rust file",
4458                    "third.rs": "// Third Rust file",
4459                }
4460            }),
4461        )
4462        .await;
4463
4464        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
4465        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4466        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4467        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4468
4469        toggle_expand_dir(&panel, "src/test", cx);
4470        select_path(&panel, "src/test/first.rs", cx);
4471        panel.update(cx, |panel, cx| panel.open(&Open, cx));
4472        cx.executor().run_until_parked();
4473        assert_eq!(
4474            visible_entries_as_strings(&panel, 0..10, cx),
4475            &[
4476                "v src",
4477                "    v test",
4478                "          first.rs  <== selected",
4479                "          second.rs",
4480                "          third.rs"
4481            ]
4482        );
4483        ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
4484
4485        submit_deletion(&panel, cx);
4486        assert_eq!(
4487            visible_entries_as_strings(&panel, 0..10, cx),
4488            &[
4489                "v src",
4490                "    v test",
4491                "          second.rs",
4492                "          third.rs"
4493            ],
4494            "Project panel should have no deleted file, no other file is selected in it"
4495        );
4496        ensure_no_open_items_and_panes(&workspace, cx);
4497
4498        select_path(&panel, "src/test/second.rs", cx);
4499        panel.update(cx, |panel, cx| panel.open(&Open, cx));
4500        cx.executor().run_until_parked();
4501        assert_eq!(
4502            visible_entries_as_strings(&panel, 0..10, cx),
4503            &[
4504                "v src",
4505                "    v test",
4506                "          second.rs  <== selected",
4507                "          third.rs"
4508            ]
4509        );
4510        ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
4511
4512        workspace
4513            .update(cx, |workspace, cx| {
4514                let active_items = workspace
4515                    .panes()
4516                    .iter()
4517                    .filter_map(|pane| pane.read(cx).active_item())
4518                    .collect::<Vec<_>>();
4519                assert_eq!(active_items.len(), 1);
4520                let open_editor = active_items
4521                    .into_iter()
4522                    .next()
4523                    .unwrap()
4524                    .downcast::<Editor>()
4525                    .expect("Open item should be an editor");
4526                open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
4527            })
4528            .unwrap();
4529        submit_deletion_skipping_prompt(&panel, cx);
4530        assert_eq!(
4531            visible_entries_as_strings(&panel, 0..10, cx),
4532            &["v src", "    v test", "          third.rs"],
4533            "Project panel should have no deleted file, with one last file remaining"
4534        );
4535        ensure_no_open_items_and_panes(&workspace, cx);
4536    }
4537
4538    #[gpui::test]
4539    async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
4540        init_test_with_editor(cx);
4541
4542        let fs = FakeFs::new(cx.executor().clone());
4543        fs.insert_tree(
4544            "/src",
4545            json!({
4546                "test": {
4547                    "first.rs": "// First Rust file",
4548                    "second.rs": "// Second Rust file",
4549                    "third.rs": "// Third Rust file",
4550                }
4551            }),
4552        )
4553        .await;
4554
4555        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
4556        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4557        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4558        let panel = workspace
4559            .update(cx, |workspace, cx| {
4560                let panel = ProjectPanel::new(workspace, cx);
4561                workspace.add_panel(panel.clone(), cx);
4562                panel
4563            })
4564            .unwrap();
4565
4566        select_path(&panel, "src/", cx);
4567        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
4568        cx.executor().run_until_parked();
4569        assert_eq!(
4570            visible_entries_as_strings(&panel, 0..10, cx),
4571            &[
4572                //
4573                "v src  <== selected",
4574                "    > test"
4575            ]
4576        );
4577        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
4578        panel.update(cx, |panel, cx| {
4579            assert!(panel.filename_editor.read(cx).is_focused(cx));
4580        });
4581        assert_eq!(
4582            visible_entries_as_strings(&panel, 0..10, cx),
4583            &[
4584                //
4585                "v src",
4586                "    > [EDITOR: '']  <== selected",
4587                "    > test"
4588            ]
4589        );
4590        panel.update(cx, |panel, cx| {
4591            panel
4592                .filename_editor
4593                .update(cx, |editor, cx| editor.set_text("test", cx));
4594            assert!(
4595                panel.confirm_edit(cx).is_none(),
4596                "Should not allow to confirm on conflicting new directory name"
4597            )
4598        });
4599        assert_eq!(
4600            visible_entries_as_strings(&panel, 0..10, cx),
4601            &[
4602                //
4603                "v src",
4604                "    > test"
4605            ],
4606            "File list should be unchanged after failed folder create confirmation"
4607        );
4608
4609        select_path(&panel, "src/test/", cx);
4610        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
4611        cx.executor().run_until_parked();
4612        assert_eq!(
4613            visible_entries_as_strings(&panel, 0..10, cx),
4614            &[
4615                //
4616                "v src",
4617                "    > test  <== selected"
4618            ]
4619        );
4620        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
4621        panel.update(cx, |panel, cx| {
4622            assert!(panel.filename_editor.read(cx).is_focused(cx));
4623        });
4624        assert_eq!(
4625            visible_entries_as_strings(&panel, 0..10, cx),
4626            &[
4627                "v src",
4628                "    v test",
4629                "          [EDITOR: '']  <== selected",
4630                "          first.rs",
4631                "          second.rs",
4632                "          third.rs"
4633            ]
4634        );
4635        panel.update(cx, |panel, cx| {
4636            panel
4637                .filename_editor
4638                .update(cx, |editor, cx| editor.set_text("first.rs", cx));
4639            assert!(
4640                panel.confirm_edit(cx).is_none(),
4641                "Should not allow to confirm on conflicting new file name"
4642            )
4643        });
4644        assert_eq!(
4645            visible_entries_as_strings(&panel, 0..10, cx),
4646            &[
4647                "v src",
4648                "    v test",
4649                "          first.rs",
4650                "          second.rs",
4651                "          third.rs"
4652            ],
4653            "File list should be unchanged after failed file create confirmation"
4654        );
4655
4656        select_path(&panel, "src/test/first.rs", cx);
4657        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
4658        cx.executor().run_until_parked();
4659        assert_eq!(
4660            visible_entries_as_strings(&panel, 0..10, cx),
4661            &[
4662                "v src",
4663                "    v test",
4664                "          first.rs  <== selected",
4665                "          second.rs",
4666                "          third.rs"
4667            ],
4668        );
4669        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
4670        panel.update(cx, |panel, cx| {
4671            assert!(panel.filename_editor.read(cx).is_focused(cx));
4672        });
4673        assert_eq!(
4674            visible_entries_as_strings(&panel, 0..10, cx),
4675            &[
4676                "v src",
4677                "    v test",
4678                "          [EDITOR: 'first.rs']  <== selected",
4679                "          second.rs",
4680                "          third.rs"
4681            ]
4682        );
4683        panel.update(cx, |panel, cx| {
4684            panel
4685                .filename_editor
4686                .update(cx, |editor, cx| editor.set_text("second.rs", cx));
4687            assert!(
4688                panel.confirm_edit(cx).is_none(),
4689                "Should not allow to confirm on conflicting file rename"
4690            )
4691        });
4692        assert_eq!(
4693            visible_entries_as_strings(&panel, 0..10, cx),
4694            &[
4695                "v src",
4696                "    v test",
4697                "          first.rs  <== selected",
4698                "          second.rs",
4699                "          third.rs"
4700            ],
4701            "File list should be unchanged after failed rename confirmation"
4702        );
4703    }
4704
4705    #[gpui::test]
4706    async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) {
4707        init_test_with_editor(cx);
4708
4709        let fs = FakeFs::new(cx.executor().clone());
4710        fs.insert_tree(
4711            "/project_root",
4712            json!({
4713                "dir_1": {
4714                    "nested_dir": {
4715                        "file_a.py": "# File contents",
4716                    }
4717                },
4718                "file_1.py": "# File contents",
4719            }),
4720        )
4721        .await;
4722
4723        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4724        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4725        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4726        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4727
4728        panel.update(cx, |panel, cx| panel.open(&Open, cx));
4729        cx.executor().run_until_parked();
4730        select_path(&panel, "project_root/dir_1", cx);
4731        panel.update(cx, |panel, cx| panel.open(&Open, cx));
4732        select_path(&panel, "project_root/dir_1/nested_dir", cx);
4733        panel.update(cx, |panel, cx| panel.open(&Open, cx));
4734        panel.update(cx, |panel, cx| panel.open(&Open, cx));
4735        cx.executor().run_until_parked();
4736        assert_eq!(
4737            visible_entries_as_strings(&panel, 0..10, cx),
4738            &[
4739                "v project_root",
4740                "    v dir_1",
4741                "        > nested_dir  <== selected",
4742                "      file_1.py",
4743            ]
4744        );
4745    }
4746
4747    #[gpui::test]
4748    async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
4749        init_test_with_editor(cx);
4750
4751        let fs = FakeFs::new(cx.executor().clone());
4752        fs.insert_tree(
4753            "/project_root",
4754            json!({
4755                "dir_1": {
4756                    "nested_dir": {
4757                        "file_a.py": "# File contents",
4758                        "file_b.py": "# File contents",
4759                        "file_c.py": "# File contents",
4760                    },
4761                    "file_1.py": "# File contents",
4762                    "file_2.py": "# File contents",
4763                    "file_3.py": "# File contents",
4764                },
4765                "dir_2": {
4766                    "file_1.py": "# File contents",
4767                    "file_2.py": "# File contents",
4768                    "file_3.py": "# File contents",
4769                }
4770            }),
4771        )
4772        .await;
4773
4774        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4775        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4776        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4777        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4778
4779        panel.update(cx, |panel, cx| {
4780            panel.collapse_all_entries(&CollapseAllEntries, cx)
4781        });
4782        cx.executor().run_until_parked();
4783        assert_eq!(
4784            visible_entries_as_strings(&panel, 0..10, cx),
4785            &["v project_root", "    > dir_1", "    > dir_2",]
4786        );
4787
4788        // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
4789        toggle_expand_dir(&panel, "project_root/dir_1", cx);
4790        cx.executor().run_until_parked();
4791        assert_eq!(
4792            visible_entries_as_strings(&panel, 0..10, cx),
4793            &[
4794                "v project_root",
4795                "    v dir_1  <== selected",
4796                "        > nested_dir",
4797                "          file_1.py",
4798                "          file_2.py",
4799                "          file_3.py",
4800                "    > dir_2",
4801            ]
4802        );
4803    }
4804
4805    #[gpui::test]
4806    async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
4807        init_test(cx);
4808
4809        let fs = FakeFs::new(cx.executor().clone());
4810        fs.as_fake().insert_tree("/root", json!({})).await;
4811        let project = Project::test(fs, ["/root".as_ref()], cx).await;
4812        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4813        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4814        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4815
4816        // Make a new buffer with no backing file
4817        workspace
4818            .update(cx, |workspace, cx| {
4819                Editor::new_file(workspace, &Default::default(), cx)
4820            })
4821            .unwrap();
4822
4823        cx.executor().run_until_parked();
4824
4825        // "Save as" the buffer, creating a new backing file for it
4826        let save_task = workspace
4827            .update(cx, |workspace, cx| {
4828                workspace.save_active_item(workspace::SaveIntent::Save, cx)
4829            })
4830            .unwrap();
4831
4832        cx.executor().run_until_parked();
4833        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new")));
4834        save_task.await.unwrap();
4835
4836        // Rename the file
4837        select_path(&panel, "root/new", cx);
4838        assert_eq!(
4839            visible_entries_as_strings(&panel, 0..10, cx),
4840            &["v root", "      new  <== selected"]
4841        );
4842        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
4843        panel.update(cx, |panel, cx| {
4844            panel
4845                .filename_editor
4846                .update(cx, |editor, cx| editor.set_text("newer", cx));
4847        });
4848        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
4849
4850        cx.executor().run_until_parked();
4851        assert_eq!(
4852            visible_entries_as_strings(&panel, 0..10, cx),
4853            &["v root", "      newer  <== selected"]
4854        );
4855
4856        workspace
4857            .update(cx, |workspace, cx| {
4858                workspace.save_active_item(workspace::SaveIntent::Save, cx)
4859            })
4860            .unwrap()
4861            .await
4862            .unwrap();
4863
4864        cx.executor().run_until_parked();
4865        // assert that saving the file doesn't restore "new"
4866        assert_eq!(
4867            visible_entries_as_strings(&panel, 0..10, cx),
4868            &["v root", "      newer  <== selected"]
4869        );
4870    }
4871
4872    #[gpui::test]
4873    async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
4874        init_test_with_editor(cx);
4875        let fs = FakeFs::new(cx.executor().clone());
4876        fs.insert_tree(
4877            "/project_root",
4878            json!({
4879                "dir_1": {
4880                    "nested_dir": {
4881                        "file_a.py": "# File contents",
4882                    }
4883                },
4884                "file_1.py": "# File contents",
4885            }),
4886        )
4887        .await;
4888
4889        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
4890        let worktree_id =
4891            cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
4892        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4893        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4894        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
4895        cx.update(|cx| {
4896            panel.update(cx, |this, cx| {
4897                this.select_next(&Default::default(), cx);
4898                this.expand_selected_entry(&Default::default(), cx);
4899                this.expand_selected_entry(&Default::default(), cx);
4900                this.select_next(&Default::default(), cx);
4901                this.expand_selected_entry(&Default::default(), cx);
4902                this.select_next(&Default::default(), cx);
4903            })
4904        });
4905        assert_eq!(
4906            visible_entries_as_strings(&panel, 0..10, cx),
4907            &[
4908                "v project_root",
4909                "    v dir_1",
4910                "        v nested_dir",
4911                "              file_a.py  <== selected",
4912                "      file_1.py",
4913            ]
4914        );
4915        let modifiers_with_shift = gpui::Modifiers {
4916            shift: true,
4917            ..Default::default()
4918        };
4919        cx.simulate_modifiers_change(modifiers_with_shift);
4920        cx.update(|cx| {
4921            panel.update(cx, |this, cx| {
4922                this.select_next(&Default::default(), cx);
4923            })
4924        });
4925        assert_eq!(
4926            visible_entries_as_strings(&panel, 0..10, cx),
4927            &[
4928                "v project_root",
4929                "    v dir_1",
4930                "        v nested_dir",
4931                "              file_a.py",
4932                "      file_1.py  <== selected  <== marked",
4933            ]
4934        );
4935        cx.update(|cx| {
4936            panel.update(cx, |this, cx| {
4937                this.select_prev(&Default::default(), cx);
4938            })
4939        });
4940        assert_eq!(
4941            visible_entries_as_strings(&panel, 0..10, cx),
4942            &[
4943                "v project_root",
4944                "    v dir_1",
4945                "        v nested_dir",
4946                "              file_a.py  <== selected  <== marked",
4947                "      file_1.py  <== marked",
4948            ]
4949        );
4950        cx.update(|cx| {
4951            panel.update(cx, |this, cx| {
4952                let drag = DraggedSelection {
4953                    active_selection: this.selection.unwrap(),
4954                    marked_selections: Arc::new(this.marked_entries.clone()),
4955                };
4956                let target_entry = this
4957                    .project
4958                    .read(cx)
4959                    .entry_for_path(&(worktree_id, "").into(), cx)
4960                    .unwrap();
4961                this.drag_onto(&drag, target_entry.id, false, cx);
4962            });
4963        });
4964        cx.run_until_parked();
4965        assert_eq!(
4966            visible_entries_as_strings(&panel, 0..10, cx),
4967            &[
4968                "v project_root",
4969                "    v dir_1",
4970                "        v nested_dir",
4971                "      file_1.py  <== marked",
4972                "      file_a.py  <== selected  <== marked",
4973            ]
4974        );
4975        // ESC clears out all marks
4976        cx.update(|cx| {
4977            panel.update(cx, |this, cx| {
4978                this.cancel(&menu::Cancel, cx);
4979            })
4980        });
4981        assert_eq!(
4982            visible_entries_as_strings(&panel, 0..10, cx),
4983            &[
4984                "v project_root",
4985                "    v dir_1",
4986                "        v nested_dir",
4987                "      file_1.py",
4988                "      file_a.py  <== selected",
4989            ]
4990        );
4991        // ESC clears out all marks
4992        cx.update(|cx| {
4993            panel.update(cx, |this, cx| {
4994                this.select_prev(&SelectPrev, cx);
4995                this.select_next(&SelectNext, cx);
4996            })
4997        });
4998        assert_eq!(
4999            visible_entries_as_strings(&panel, 0..10, cx),
5000            &[
5001                "v project_root",
5002                "    v dir_1",
5003                "        v nested_dir",
5004                "      file_1.py  <== marked",
5005                "      file_a.py  <== selected  <== marked",
5006            ]
5007        );
5008        cx.simulate_modifiers_change(Default::default());
5009        cx.update(|cx| {
5010            panel.update(cx, |this, cx| {
5011                this.cut(&Cut, cx);
5012                this.select_prev(&SelectPrev, cx);
5013                this.select_prev(&SelectPrev, cx);
5014
5015                this.paste(&Paste, cx);
5016                // this.expand_selected_entry(&ExpandSelectedEntry, cx);
5017            })
5018        });
5019        cx.run_until_parked();
5020        assert_eq!(
5021            visible_entries_as_strings(&panel, 0..10, cx),
5022            &[
5023                "v project_root",
5024                "    v dir_1",
5025                "        v nested_dir",
5026                "              file_1.py  <== marked",
5027                "              file_a.py  <== selected  <== marked",
5028            ]
5029        );
5030        cx.simulate_modifiers_change(modifiers_with_shift);
5031        cx.update(|cx| {
5032            panel.update(cx, |this, cx| {
5033                this.expand_selected_entry(&Default::default(), cx);
5034                this.select_next(&SelectNext, cx);
5035                this.select_next(&SelectNext, cx);
5036            })
5037        });
5038        submit_deletion(&panel, cx);
5039        assert_eq!(
5040            visible_entries_as_strings(&panel, 0..10, cx),
5041            &["v project_root", "    v dir_1", "        v nested_dir",]
5042        );
5043    }
5044    #[gpui::test]
5045    async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
5046        init_test_with_editor(cx);
5047        cx.update(|cx| {
5048            cx.update_global::<SettingsStore, _>(|store, cx| {
5049                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5050                    worktree_settings.file_scan_exclusions = Some(Vec::new());
5051                });
5052                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5053                    project_panel_settings.auto_reveal_entries = Some(false)
5054                });
5055            })
5056        });
5057
5058        let fs = FakeFs::new(cx.background_executor.clone());
5059        fs.insert_tree(
5060            "/project_root",
5061            json!({
5062                ".git": {},
5063                ".gitignore": "**/gitignored_dir",
5064                "dir_1": {
5065                    "file_1.py": "# File 1_1 contents",
5066                    "file_2.py": "# File 1_2 contents",
5067                    "file_3.py": "# File 1_3 contents",
5068                    "gitignored_dir": {
5069                        "file_a.py": "# File contents",
5070                        "file_b.py": "# File contents",
5071                        "file_c.py": "# File contents",
5072                    },
5073                },
5074                "dir_2": {
5075                    "file_1.py": "# File 2_1 contents",
5076                    "file_2.py": "# File 2_2 contents",
5077                    "file_3.py": "# File 2_3 contents",
5078                }
5079            }),
5080        )
5081        .await;
5082
5083        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
5084        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5085        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5086        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5087
5088        assert_eq!(
5089            visible_entries_as_strings(&panel, 0..20, cx),
5090            &[
5091                "v project_root",
5092                "    > .git",
5093                "    > dir_1",
5094                "    > dir_2",
5095                "      .gitignore",
5096            ]
5097        );
5098
5099        let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
5100            .expect("dir 1 file is not ignored and should have an entry");
5101        let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
5102            .expect("dir 2 file is not ignored and should have an entry");
5103        let gitignored_dir_file =
5104            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
5105        assert_eq!(
5106            gitignored_dir_file, None,
5107            "File in the gitignored dir should not have an entry before its dir is toggled"
5108        );
5109
5110        toggle_expand_dir(&panel, "project_root/dir_1", cx);
5111        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
5112        cx.executor().run_until_parked();
5113        assert_eq!(
5114            visible_entries_as_strings(&panel, 0..20, cx),
5115            &[
5116                "v project_root",
5117                "    > .git",
5118                "    v dir_1",
5119                "        v gitignored_dir  <== selected",
5120                "              file_a.py",
5121                "              file_b.py",
5122                "              file_c.py",
5123                "          file_1.py",
5124                "          file_2.py",
5125                "          file_3.py",
5126                "    > dir_2",
5127                "      .gitignore",
5128            ],
5129            "Should show gitignored dir file list in the project panel"
5130        );
5131        let gitignored_dir_file =
5132            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
5133                .expect("after gitignored dir got opened, a file entry should be present");
5134
5135        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
5136        toggle_expand_dir(&panel, "project_root/dir_1", cx);
5137        assert_eq!(
5138            visible_entries_as_strings(&panel, 0..20, cx),
5139            &[
5140                "v project_root",
5141                "    > .git",
5142                "    > dir_1  <== selected",
5143                "    > dir_2",
5144                "      .gitignore",
5145            ],
5146            "Should hide all dir contents again and prepare for the auto reveal test"
5147        );
5148
5149        for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
5150            panel.update(cx, |panel, cx| {
5151                panel.project.update(cx, |_, cx| {
5152                    cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
5153                })
5154            });
5155            cx.run_until_parked();
5156            assert_eq!(
5157                visible_entries_as_strings(&panel, 0..20, cx),
5158                &[
5159                    "v project_root",
5160                    "    > .git",
5161                    "    > dir_1  <== selected",
5162                    "    > dir_2",
5163                    "      .gitignore",
5164                ],
5165                "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
5166            );
5167        }
5168
5169        cx.update(|cx| {
5170            cx.update_global::<SettingsStore, _>(|store, cx| {
5171                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5172                    project_panel_settings.auto_reveal_entries = Some(true)
5173                });
5174            })
5175        });
5176
5177        panel.update(cx, |panel, cx| {
5178            panel.project.update(cx, |_, cx| {
5179                cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
5180            })
5181        });
5182        cx.run_until_parked();
5183        assert_eq!(
5184            visible_entries_as_strings(&panel, 0..20, cx),
5185            &[
5186                "v project_root",
5187                "    > .git",
5188                "    v dir_1",
5189                "        > gitignored_dir",
5190                "          file_1.py  <== selected",
5191                "          file_2.py",
5192                "          file_3.py",
5193                "    > dir_2",
5194                "      .gitignore",
5195            ],
5196            "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
5197        );
5198
5199        panel.update(cx, |panel, cx| {
5200            panel.project.update(cx, |_, cx| {
5201                cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
5202            })
5203        });
5204        cx.run_until_parked();
5205        assert_eq!(
5206            visible_entries_as_strings(&panel, 0..20, cx),
5207            &[
5208                "v project_root",
5209                "    > .git",
5210                "    v dir_1",
5211                "        > gitignored_dir",
5212                "          file_1.py",
5213                "          file_2.py",
5214                "          file_3.py",
5215                "    v dir_2",
5216                "          file_1.py  <== selected",
5217                "          file_2.py",
5218                "          file_3.py",
5219                "      .gitignore",
5220            ],
5221            "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
5222        );
5223
5224        panel.update(cx, |panel, cx| {
5225            panel.project.update(cx, |_, cx| {
5226                cx.emit(project::Event::ActiveEntryChanged(Some(
5227                    gitignored_dir_file,
5228                )))
5229            })
5230        });
5231        cx.run_until_parked();
5232        assert_eq!(
5233            visible_entries_as_strings(&panel, 0..20, cx),
5234            &[
5235                "v project_root",
5236                "    > .git",
5237                "    v dir_1",
5238                "        > gitignored_dir",
5239                "          file_1.py",
5240                "          file_2.py",
5241                "          file_3.py",
5242                "    v dir_2",
5243                "          file_1.py  <== selected",
5244                "          file_2.py",
5245                "          file_3.py",
5246                "      .gitignore",
5247            ],
5248            "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
5249        );
5250
5251        panel.update(cx, |panel, cx| {
5252            panel.project.update(cx, |_, cx| {
5253                cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
5254            })
5255        });
5256        cx.run_until_parked();
5257        assert_eq!(
5258            visible_entries_as_strings(&panel, 0..20, cx),
5259            &[
5260                "v project_root",
5261                "    > .git",
5262                "    v dir_1",
5263                "        v gitignored_dir",
5264                "              file_a.py  <== selected",
5265                "              file_b.py",
5266                "              file_c.py",
5267                "          file_1.py",
5268                "          file_2.py",
5269                "          file_3.py",
5270                "    v dir_2",
5271                "          file_1.py",
5272                "          file_2.py",
5273                "          file_3.py",
5274                "      .gitignore",
5275            ],
5276            "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
5277        );
5278    }
5279
5280    #[gpui::test]
5281    async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
5282        init_test_with_editor(cx);
5283        cx.update(|cx| {
5284            cx.update_global::<SettingsStore, _>(|store, cx| {
5285                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5286                    worktree_settings.file_scan_exclusions = Some(Vec::new());
5287                });
5288                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5289                    project_panel_settings.auto_reveal_entries = Some(false)
5290                });
5291            })
5292        });
5293
5294        let fs = FakeFs::new(cx.background_executor.clone());
5295        fs.insert_tree(
5296            "/project_root",
5297            json!({
5298                ".git": {},
5299                ".gitignore": "**/gitignored_dir",
5300                "dir_1": {
5301                    "file_1.py": "# File 1_1 contents",
5302                    "file_2.py": "# File 1_2 contents",
5303                    "file_3.py": "# File 1_3 contents",
5304                    "gitignored_dir": {
5305                        "file_a.py": "# File contents",
5306                        "file_b.py": "# File contents",
5307                        "file_c.py": "# File contents",
5308                    },
5309                },
5310                "dir_2": {
5311                    "file_1.py": "# File 2_1 contents",
5312                    "file_2.py": "# File 2_2 contents",
5313                    "file_3.py": "# File 2_3 contents",
5314                }
5315            }),
5316        )
5317        .await;
5318
5319        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
5320        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5321        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5322        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
5323
5324        assert_eq!(
5325            visible_entries_as_strings(&panel, 0..20, cx),
5326            &[
5327                "v project_root",
5328                "    > .git",
5329                "    > dir_1",
5330                "    > dir_2",
5331                "      .gitignore",
5332            ]
5333        );
5334
5335        let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
5336            .expect("dir 1 file is not ignored and should have an entry");
5337        let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
5338            .expect("dir 2 file is not ignored and should have an entry");
5339        let gitignored_dir_file =
5340            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
5341        assert_eq!(
5342            gitignored_dir_file, None,
5343            "File in the gitignored dir should not have an entry before its dir is toggled"
5344        );
5345
5346        toggle_expand_dir(&panel, "project_root/dir_1", cx);
5347        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
5348        cx.run_until_parked();
5349        assert_eq!(
5350            visible_entries_as_strings(&panel, 0..20, cx),
5351            &[
5352                "v project_root",
5353                "    > .git",
5354                "    v dir_1",
5355                "        v gitignored_dir  <== selected",
5356                "              file_a.py",
5357                "              file_b.py",
5358                "              file_c.py",
5359                "          file_1.py",
5360                "          file_2.py",
5361                "          file_3.py",
5362                "    > dir_2",
5363                "      .gitignore",
5364            ],
5365            "Should show gitignored dir file list in the project panel"
5366        );
5367        let gitignored_dir_file =
5368            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
5369                .expect("after gitignored dir got opened, a file entry should be present");
5370
5371        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
5372        toggle_expand_dir(&panel, "project_root/dir_1", cx);
5373        assert_eq!(
5374            visible_entries_as_strings(&panel, 0..20, cx),
5375            &[
5376                "v project_root",
5377                "    > .git",
5378                "    > dir_1  <== selected",
5379                "    > dir_2",
5380                "      .gitignore",
5381            ],
5382            "Should hide all dir contents again and prepare for the explicit reveal test"
5383        );
5384
5385        for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
5386            panel.update(cx, |panel, cx| {
5387                panel.project.update(cx, |_, cx| {
5388                    cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
5389                })
5390            });
5391            cx.run_until_parked();
5392            assert_eq!(
5393                visible_entries_as_strings(&panel, 0..20, cx),
5394                &[
5395                    "v project_root",
5396                    "    > .git",
5397                    "    > dir_1  <== selected",
5398                    "    > dir_2",
5399                    "      .gitignore",
5400                ],
5401                "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
5402            );
5403        }
5404
5405        panel.update(cx, |panel, cx| {
5406            panel.project.update(cx, |_, cx| {
5407                cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
5408            })
5409        });
5410        cx.run_until_parked();
5411        assert_eq!(
5412            visible_entries_as_strings(&panel, 0..20, cx),
5413            &[
5414                "v project_root",
5415                "    > .git",
5416                "    v dir_1",
5417                "        > gitignored_dir",
5418                "          file_1.py  <== selected",
5419                "          file_2.py",
5420                "          file_3.py",
5421                "    > dir_2",
5422                "      .gitignore",
5423            ],
5424            "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
5425        );
5426
5427        panel.update(cx, |panel, cx| {
5428            panel.project.update(cx, |_, cx| {
5429                cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
5430            })
5431        });
5432        cx.run_until_parked();
5433        assert_eq!(
5434            visible_entries_as_strings(&panel, 0..20, cx),
5435            &[
5436                "v project_root",
5437                "    > .git",
5438                "    v dir_1",
5439                "        > gitignored_dir",
5440                "          file_1.py",
5441                "          file_2.py",
5442                "          file_3.py",
5443                "    v dir_2",
5444                "          file_1.py  <== selected",
5445                "          file_2.py",
5446                "          file_3.py",
5447                "      .gitignore",
5448            ],
5449            "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
5450        );
5451
5452        panel.update(cx, |panel, cx| {
5453            panel.project.update(cx, |_, cx| {
5454                cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
5455            })
5456        });
5457        cx.run_until_parked();
5458        assert_eq!(
5459            visible_entries_as_strings(&panel, 0..20, cx),
5460            &[
5461                "v project_root",
5462                "    > .git",
5463                "    v dir_1",
5464                "        v gitignored_dir",
5465                "              file_a.py  <== selected",
5466                "              file_b.py",
5467                "              file_c.py",
5468                "          file_1.py",
5469                "          file_2.py",
5470                "          file_3.py",
5471                "    v dir_2",
5472                "          file_1.py",
5473                "          file_2.py",
5474                "          file_3.py",
5475                "      .gitignore",
5476            ],
5477            "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
5478        );
5479    }
5480
5481    #[gpui::test]
5482    async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
5483        init_test(cx);
5484        cx.update(|cx| {
5485            cx.update_global::<SettingsStore, _>(|store, cx| {
5486                store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
5487                    project_settings.file_scan_exclusions =
5488                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
5489                });
5490            });
5491        });
5492
5493        cx.update(|cx| {
5494            register_project_item::<TestProjectItemView>(cx);
5495        });
5496
5497        let fs = FakeFs::new(cx.executor().clone());
5498        fs.insert_tree(
5499            "/root1",
5500            json!({
5501                ".dockerignore": "",
5502                ".git": {
5503                    "HEAD": "",
5504                },
5505            }),
5506        )
5507        .await;
5508
5509        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
5510        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5511        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5512        let panel = workspace
5513            .update(cx, |workspace, cx| {
5514                let panel = ProjectPanel::new(workspace, cx);
5515                workspace.add_panel(panel.clone(), cx);
5516                panel
5517            })
5518            .unwrap();
5519
5520        select_path(&panel, "root1", cx);
5521        assert_eq!(
5522            visible_entries_as_strings(&panel, 0..10, cx),
5523            &["v root1  <== selected", "      .dockerignore",]
5524        );
5525        workspace
5526            .update(cx, |workspace, cx| {
5527                assert!(
5528                    workspace.active_item(cx).is_none(),
5529                    "Should have no active items in the beginning"
5530                );
5531            })
5532            .unwrap();
5533
5534        let excluded_file_path = ".git/COMMIT_EDITMSG";
5535        let excluded_dir_path = "excluded_dir";
5536
5537        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
5538        panel.update(cx, |panel, cx| {
5539            assert!(panel.filename_editor.read(cx).is_focused(cx));
5540        });
5541        panel
5542            .update(cx, |panel, cx| {
5543                panel
5544                    .filename_editor
5545                    .update(cx, |editor, cx| editor.set_text(excluded_file_path, cx));
5546                panel.confirm_edit(cx).unwrap()
5547            })
5548            .await
5549            .unwrap();
5550
5551        assert_eq!(
5552            visible_entries_as_strings(&panel, 0..13, cx),
5553            &["v root1", "      .dockerignore"],
5554            "Excluded dir should not be shown after opening a file in it"
5555        );
5556        panel.update(cx, |panel, cx| {
5557            assert!(
5558                !panel.filename_editor.read(cx).is_focused(cx),
5559                "Should have closed the file name editor"
5560            );
5561        });
5562        workspace
5563            .update(cx, |workspace, cx| {
5564                let active_entry_path = workspace
5565                    .active_item(cx)
5566                    .expect("should have opened and activated the excluded item")
5567                    .act_as::<TestProjectItemView>(cx)
5568                    .expect(
5569                        "should have opened the corresponding project item for the excluded item",
5570                    )
5571                    .read(cx)
5572                    .path
5573                    .clone();
5574                assert_eq!(
5575                    active_entry_path.path.as_ref(),
5576                    Path::new(excluded_file_path),
5577                    "Should open the excluded file"
5578                );
5579
5580                assert!(
5581                    workspace.notification_ids().is_empty(),
5582                    "Should have no notifications after opening an excluded file"
5583                );
5584            })
5585            .unwrap();
5586        assert!(
5587            fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
5588            "Should have created the excluded file"
5589        );
5590
5591        select_path(&panel, "root1", cx);
5592        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
5593        panel.update(cx, |panel, cx| {
5594            assert!(panel.filename_editor.read(cx).is_focused(cx));
5595        });
5596        panel
5597            .update(cx, |panel, cx| {
5598                panel
5599                    .filename_editor
5600                    .update(cx, |editor, cx| editor.set_text(excluded_file_path, cx));
5601                panel.confirm_edit(cx).unwrap()
5602            })
5603            .await
5604            .unwrap();
5605
5606        assert_eq!(
5607            visible_entries_as_strings(&panel, 0..13, cx),
5608            &["v root1", "      .dockerignore"],
5609            "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
5610        );
5611        panel.update(cx, |panel, cx| {
5612            assert!(
5613                !panel.filename_editor.read(cx).is_focused(cx),
5614                "Should have closed the file name editor"
5615            );
5616        });
5617        workspace
5618            .update(cx, |workspace, cx| {
5619                let notifications = workspace.notification_ids();
5620                assert_eq!(
5621                    notifications.len(),
5622                    1,
5623                    "Should receive one notification with the error message"
5624                );
5625                workspace.dismiss_notification(notifications.first().unwrap(), cx);
5626                assert!(workspace.notification_ids().is_empty());
5627            })
5628            .unwrap();
5629
5630        select_path(&panel, "root1", cx);
5631        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
5632        panel.update(cx, |panel, cx| {
5633            assert!(panel.filename_editor.read(cx).is_focused(cx));
5634        });
5635        panel
5636            .update(cx, |panel, cx| {
5637                panel
5638                    .filename_editor
5639                    .update(cx, |editor, cx| editor.set_text(excluded_dir_path, cx));
5640                panel.confirm_edit(cx).unwrap()
5641            })
5642            .await
5643            .unwrap();
5644
5645        assert_eq!(
5646            visible_entries_as_strings(&panel, 0..13, cx),
5647            &["v root1", "      .dockerignore"],
5648            "Should not change the project panel after trying to create an excluded directory"
5649        );
5650        panel.update(cx, |panel, cx| {
5651            assert!(
5652                !panel.filename_editor.read(cx).is_focused(cx),
5653                "Should have closed the file name editor"
5654            );
5655        });
5656        workspace
5657            .update(cx, |workspace, cx| {
5658                let notifications = workspace.notification_ids();
5659                assert_eq!(
5660                    notifications.len(),
5661                    1,
5662                    "Should receive one notification explaining that no directory is actually shown"
5663                );
5664                workspace.dismiss_notification(notifications.first().unwrap(), cx);
5665                assert!(workspace.notification_ids().is_empty());
5666            })
5667            .unwrap();
5668        assert!(
5669            fs.is_dir(Path::new("/root1/excluded_dir")).await,
5670            "Should have created the excluded directory"
5671        );
5672    }
5673
5674    fn toggle_expand_dir(
5675        panel: &View<ProjectPanel>,
5676        path: impl AsRef<Path>,
5677        cx: &mut VisualTestContext,
5678    ) {
5679        let path = path.as_ref();
5680        panel.update(cx, |panel, cx| {
5681            for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5682                let worktree = worktree.read(cx);
5683                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5684                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
5685                    panel.toggle_expanded(entry_id, cx);
5686                    return;
5687                }
5688            }
5689            panic!("no worktree for path {:?}", path);
5690        });
5691    }
5692
5693    fn select_path(panel: &View<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
5694        let path = path.as_ref();
5695        panel.update(cx, |panel, cx| {
5696            for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5697                let worktree = worktree.read(cx);
5698                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5699                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
5700                    panel.selection = Some(crate::SelectedEntry {
5701                        worktree_id: worktree.id(),
5702                        entry_id,
5703                    });
5704                    return;
5705                }
5706            }
5707            panic!("no worktree for path {:?}", path);
5708        });
5709    }
5710
5711    fn find_project_entry(
5712        panel: &View<ProjectPanel>,
5713        path: impl AsRef<Path>,
5714        cx: &mut VisualTestContext,
5715    ) -> Option<ProjectEntryId> {
5716        let path = path.as_ref();
5717        panel.update(cx, |panel, cx| {
5718            for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
5719                let worktree = worktree.read(cx);
5720                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
5721                    return worktree.entry_for_path(relative_path).map(|entry| entry.id);
5722                }
5723            }
5724            panic!("no worktree for path {path:?}");
5725        })
5726    }
5727
5728    fn visible_entries_as_strings(
5729        panel: &View<ProjectPanel>,
5730        range: Range<usize>,
5731        cx: &mut VisualTestContext,
5732    ) -> Vec<String> {
5733        let mut result = Vec::new();
5734        let mut project_entries = HashSet::default();
5735        let mut has_editor = false;
5736
5737        panel.update(cx, |panel, cx| {
5738            panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
5739                if details.is_editing {
5740                    assert!(!has_editor, "duplicate editor entry");
5741                    has_editor = true;
5742                } else {
5743                    assert!(
5744                        project_entries.insert(project_entry),
5745                        "duplicate project entry {:?} {:?}",
5746                        project_entry,
5747                        details
5748                    );
5749                }
5750
5751                let indent = "    ".repeat(details.depth);
5752                let icon = if details.kind.is_dir() {
5753                    if details.is_expanded {
5754                        "v "
5755                    } else {
5756                        "> "
5757                    }
5758                } else {
5759                    "  "
5760                };
5761                let name = if details.is_editing {
5762                    format!("[EDITOR: '{}']", details.filename)
5763                } else if details.is_processing {
5764                    format!("[PROCESSING: '{}']", details.filename)
5765                } else {
5766                    details.filename.clone()
5767                };
5768                let selected = if details.is_selected {
5769                    "  <== selected"
5770                } else {
5771                    ""
5772                };
5773                let marked = if details.is_marked {
5774                    "  <== marked"
5775                } else {
5776                    ""
5777                };
5778
5779                result.push(format!("{indent}{icon}{name}{selected}{marked}"));
5780            });
5781        });
5782
5783        result
5784    }
5785
5786    fn init_test(cx: &mut TestAppContext) {
5787        cx.update(|cx| {
5788            let settings_store = SettingsStore::test(cx);
5789            cx.set_global(settings_store);
5790            init_settings(cx);
5791            theme::init(theme::LoadThemes::JustBase, cx);
5792            language::init(cx);
5793            editor::init_settings(cx);
5794            crate::init((), cx);
5795            workspace::init_settings(cx);
5796            client::init_settings(cx);
5797            Project::init_settings(cx);
5798
5799            cx.update_global::<SettingsStore, _>(|store, cx| {
5800                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5801                    project_panel_settings.auto_fold_dirs = Some(false);
5802                });
5803                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5804                    worktree_settings.file_scan_exclusions = Some(Vec::new());
5805                });
5806            });
5807        });
5808    }
5809
5810    fn init_test_with_editor(cx: &mut TestAppContext) {
5811        cx.update(|cx| {
5812            let app_state = AppState::test(cx);
5813            theme::init(theme::LoadThemes::JustBase, cx);
5814            init_settings(cx);
5815            language::init(cx);
5816            editor::init(cx);
5817            crate::init((), cx);
5818            workspace::init(app_state.clone(), cx);
5819            Project::init_settings(cx);
5820
5821            cx.update_global::<SettingsStore, _>(|store, cx| {
5822                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
5823                    project_panel_settings.auto_fold_dirs = Some(false);
5824                });
5825                store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
5826                    worktree_settings.file_scan_exclusions = Some(Vec::new());
5827                });
5828            });
5829        });
5830    }
5831
5832    fn ensure_single_file_is_opened(
5833        window: &WindowHandle<Workspace>,
5834        expected_path: &str,
5835        cx: &mut TestAppContext,
5836    ) {
5837        window
5838            .update(cx, |workspace, cx| {
5839                let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
5840                assert_eq!(worktrees.len(), 1);
5841                let worktree_id = worktrees[0].read(cx).id();
5842
5843                let open_project_paths = workspace
5844                    .panes()
5845                    .iter()
5846                    .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5847                    .collect::<Vec<_>>();
5848                assert_eq!(
5849                    open_project_paths,
5850                    vec![ProjectPath {
5851                        worktree_id,
5852                        path: Arc::from(Path::new(expected_path))
5853                    }],
5854                    "Should have opened file, selected in project panel"
5855                );
5856            })
5857            .unwrap();
5858    }
5859
5860    fn submit_deletion(panel: &View<ProjectPanel>, cx: &mut VisualTestContext) {
5861        assert!(
5862            !cx.has_pending_prompt(),
5863            "Should have no prompts before the deletion"
5864        );
5865        panel.update(cx, |panel, cx| {
5866            panel.delete(&Delete { skip_prompt: false }, cx)
5867        });
5868        assert!(
5869            cx.has_pending_prompt(),
5870            "Should have a prompt after the deletion"
5871        );
5872        cx.simulate_prompt_answer(0);
5873        assert!(
5874            !cx.has_pending_prompt(),
5875            "Should have no prompts after prompt was replied to"
5876        );
5877        cx.executor().run_until_parked();
5878    }
5879
5880    fn submit_deletion_skipping_prompt(panel: &View<ProjectPanel>, cx: &mut VisualTestContext) {
5881        assert!(
5882            !cx.has_pending_prompt(),
5883            "Should have no prompts before the deletion"
5884        );
5885        panel.update(cx, |panel, cx| {
5886            panel.delete(&Delete { skip_prompt: true }, cx)
5887        });
5888        assert!(!cx.has_pending_prompt(), "Should have received no prompts");
5889        cx.executor().run_until_parked();
5890    }
5891
5892    fn ensure_no_open_items_and_panes(
5893        workspace: &WindowHandle<Workspace>,
5894        cx: &mut VisualTestContext,
5895    ) {
5896        assert!(
5897            !cx.has_pending_prompt(),
5898            "Should have no prompts after deletion operation closes the file"
5899        );
5900        workspace
5901            .read_with(cx, |workspace, cx| {
5902                let open_project_paths = workspace
5903                    .panes()
5904                    .iter()
5905                    .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
5906                    .collect::<Vec<_>>();
5907                assert!(
5908                    open_project_paths.is_empty(),
5909                    "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
5910                );
5911            })
5912            .unwrap();
5913    }
5914
5915    struct TestProjectItemView {
5916        focus_handle: FocusHandle,
5917        path: ProjectPath,
5918    }
5919
5920    struct TestProjectItem {
5921        path: ProjectPath,
5922    }
5923
5924    impl project::Item for TestProjectItem {
5925        fn try_open(
5926            _project: &Model<Project>,
5927            path: &ProjectPath,
5928            cx: &mut AppContext,
5929        ) -> Option<Task<gpui::Result<Model<Self>>>> {
5930            let path = path.clone();
5931            Some(cx.spawn(|mut cx| async move { cx.new_model(|_| Self { path }) }))
5932        }
5933
5934        fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
5935            None
5936        }
5937
5938        fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
5939            Some(self.path.clone())
5940        }
5941    }
5942
5943    impl ProjectItem for TestProjectItemView {
5944        type Item = TestProjectItem;
5945
5946        fn for_project_item(
5947            _: Model<Project>,
5948            project_item: Model<Self::Item>,
5949            cx: &mut ViewContext<Self>,
5950        ) -> Self
5951        where
5952            Self: Sized,
5953        {
5954            Self {
5955                path: project_item.update(cx, |project_item, _| project_item.path.clone()),
5956                focus_handle: cx.focus_handle(),
5957            }
5958        }
5959    }
5960
5961    impl Item for TestProjectItemView {
5962        type Event = ();
5963    }
5964
5965    impl EventEmitter<()> for TestProjectItemView {}
5966
5967    impl FocusableView for TestProjectItemView {
5968        fn focus_handle(&self, _: &AppContext) -> FocusHandle {
5969            self.focus_handle.clone()
5970        }
5971    }
5972
5973    impl Render for TestProjectItemView {
5974        fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
5975            Empty
5976        }
5977    }
5978}