project_panel.rs

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