project_panel.rs

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