main.rs

   1mod worktree_settings;
   2
   3use anyhow::Result;
   4use encoding_rs;
   5use fs::{FakeFs, Fs, RealFs, RemoveOptions};
   6use git::{DOT_GIT, GITIGNORE, REPO_EXCLUDE};
   7use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext};
   8use parking_lot::Mutex;
   9use postage::stream::Stream;
  10use pretty_assertions::assert_eq;
  11use rand::prelude::*;
  12use worktree::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle};
  13
  14use serde_json::json;
  15use settings::{SettingsStore, WorktreeId};
  16use std::{
  17    env,
  18    fmt::Write,
  19    mem,
  20    path::{Path, PathBuf},
  21    sync::Arc,
  22};
  23use util::{
  24    ResultExt, path,
  25    paths::PathStyle,
  26    rel_path::{RelPath, rel_path},
  27    test::TempTree,
  28};
  29
  30#[gpui::test]
  31async fn test_traversal(cx: &mut TestAppContext) {
  32    init_test(cx);
  33    let fs = FakeFs::new(cx.background_executor.clone());
  34    fs.insert_tree(
  35        "/root",
  36        json!({
  37           ".gitignore": "a/b\n",
  38           "a": {
  39               "b": "",
  40               "c": "",
  41           }
  42        }),
  43    )
  44    .await;
  45
  46    let tree = Worktree::local(
  47        Path::new("/root"),
  48        true,
  49        fs,
  50        Default::default(),
  51        true,
  52        WorktreeId::from_proto(0),
  53        &mut cx.to_async(),
  54    )
  55    .await
  56    .unwrap();
  57    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
  58        .await;
  59
  60    tree.read_with(cx, |tree, _| {
  61        assert_eq!(
  62            tree.entries(false, 0)
  63                .map(|entry| entry.path.as_ref())
  64                .collect::<Vec<_>>(),
  65            vec![
  66                rel_path(""),
  67                rel_path(".gitignore"),
  68                rel_path("a"),
  69                rel_path("a/c"),
  70            ]
  71        );
  72        assert_eq!(
  73            tree.entries(true, 0)
  74                .map(|entry| entry.path.as_ref())
  75                .collect::<Vec<_>>(),
  76            vec![
  77                rel_path(""),
  78                rel_path(".gitignore"),
  79                rel_path("a"),
  80                rel_path("a/b"),
  81                rel_path("a/c"),
  82            ]
  83        );
  84    })
  85}
  86
  87#[gpui::test(iterations = 10)]
  88async fn test_circular_symlinks(cx: &mut TestAppContext) {
  89    init_test(cx);
  90    let fs = FakeFs::new(cx.background_executor.clone());
  91    fs.insert_tree(
  92        "/root",
  93        json!({
  94            "lib": {
  95                "a": {
  96                    "a.txt": ""
  97                },
  98                "b": {
  99                    "b.txt": ""
 100                }
 101            }
 102        }),
 103    )
 104    .await;
 105    fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into())
 106        .await
 107        .unwrap();
 108    fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into())
 109        .await
 110        .unwrap();
 111
 112    let tree = Worktree::local(
 113        Path::new("/root"),
 114        true,
 115        fs.clone(),
 116        Default::default(),
 117        true,
 118        WorktreeId::from_proto(0),
 119        &mut cx.to_async(),
 120    )
 121    .await
 122    .unwrap();
 123
 124    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 125        .await;
 126
 127    tree.read_with(cx, |tree, _| {
 128        assert_eq!(
 129            tree.entries(false, 0)
 130                .map(|entry| entry.path.as_ref())
 131                .collect::<Vec<_>>(),
 132            vec![
 133                rel_path(""),
 134                rel_path("lib"),
 135                rel_path("lib/a"),
 136                rel_path("lib/a/a.txt"),
 137                rel_path("lib/a/lib"),
 138                rel_path("lib/b"),
 139                rel_path("lib/b/b.txt"),
 140                rel_path("lib/b/lib"),
 141            ]
 142        );
 143    });
 144
 145    fs.rename(
 146        Path::new("/root/lib/a/lib"),
 147        Path::new("/root/lib/a/lib-2"),
 148        Default::default(),
 149    )
 150    .await
 151    .unwrap();
 152    cx.executor().run_until_parked();
 153    tree.read_with(cx, |tree, _| {
 154        assert_eq!(
 155            tree.entries(false, 0)
 156                .map(|entry| entry.path.as_ref())
 157                .collect::<Vec<_>>(),
 158            vec![
 159                rel_path(""),
 160                rel_path("lib"),
 161                rel_path("lib/a"),
 162                rel_path("lib/a/a.txt"),
 163                rel_path("lib/a/lib-2"),
 164                rel_path("lib/b"),
 165                rel_path("lib/b/b.txt"),
 166                rel_path("lib/b/lib"),
 167            ]
 168        );
 169    });
 170}
 171
 172#[gpui::test]
 173async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
 174    init_test(cx);
 175    let fs = FakeFs::new(cx.background_executor.clone());
 176    fs.insert_tree(
 177        "/root",
 178        json!({
 179            "dir1": {
 180                "deps": {
 181                    // symlinks here
 182                },
 183                "src": {
 184                    "a.rs": "",
 185                    "b.rs": "",
 186                },
 187            },
 188            "dir2": {
 189                "src": {
 190                    "c.rs": "",
 191                    "d.rs": "",
 192                }
 193            },
 194            "dir3": {
 195                "deps": {},
 196                "src": {
 197                    "e.rs": "",
 198                    "f.rs": "",
 199                },
 200            }
 201        }),
 202    )
 203    .await;
 204
 205    // These symlinks point to directories outside of the worktree's root, dir1.
 206    fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
 207        .await
 208        .unwrap();
 209    fs.create_symlink("/root/dir1/deps/dep-dir3".as_ref(), "../../dir3".into())
 210        .await
 211        .unwrap();
 212
 213    let tree = Worktree::local(
 214        Path::new("/root/dir1"),
 215        true,
 216        fs.clone(),
 217        Default::default(),
 218        true,
 219        WorktreeId::from_proto(0),
 220        &mut cx.to_async(),
 221    )
 222    .await
 223    .unwrap();
 224
 225    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 226        .await;
 227
 228    let tree_updates = Arc::new(Mutex::new(Vec::new()));
 229    tree.update(cx, |_, cx| {
 230        let tree_updates = tree_updates.clone();
 231        cx.subscribe(&tree, move |_, _, event, _| {
 232            if let Event::UpdatedEntries(update) = event {
 233                tree_updates.lock().extend(
 234                    update
 235                        .iter()
 236                        .map(|(path, _, change)| (path.clone(), *change)),
 237                );
 238            }
 239        })
 240        .detach();
 241    });
 242
 243    // The symlinked directories are not scanned by default.
 244    tree.read_with(cx, |tree, _| {
 245        assert_eq!(
 246            tree.entries(true, 0)
 247                .map(|entry| (entry.path.as_ref(), entry.is_external))
 248                .collect::<Vec<_>>(),
 249            vec![
 250                (rel_path(""), false),
 251                (rel_path("deps"), false),
 252                (rel_path("deps/dep-dir2"), true),
 253                (rel_path("deps/dep-dir3"), true),
 254                (rel_path("src"), false),
 255                (rel_path("src/a.rs"), false),
 256                (rel_path("src/b.rs"), false),
 257            ]
 258        );
 259
 260        assert_eq!(
 261            tree.entry_for_path(rel_path("deps/dep-dir2")).unwrap().kind,
 262            EntryKind::UnloadedDir
 263        );
 264    });
 265
 266    // Expand one of the symlinked directories.
 267    tree.read_with(cx, |tree, _| {
 268        tree.as_local()
 269            .unwrap()
 270            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3").into()])
 271    })
 272    .recv()
 273    .await;
 274
 275    // The expanded directory's contents are loaded. Subdirectories are
 276    // not scanned yet.
 277    tree.read_with(cx, |tree, _| {
 278        assert_eq!(
 279            tree.entries(true, 0)
 280                .map(|entry| (entry.path.as_ref(), entry.is_external))
 281                .collect::<Vec<_>>(),
 282            vec![
 283                (rel_path(""), false),
 284                (rel_path("deps"), false),
 285                (rel_path("deps/dep-dir2"), true),
 286                (rel_path("deps/dep-dir3"), true),
 287                (rel_path("deps/dep-dir3/deps"), true),
 288                (rel_path("deps/dep-dir3/src"), true),
 289                (rel_path("src"), false),
 290                (rel_path("src/a.rs"), false),
 291                (rel_path("src/b.rs"), false),
 292            ]
 293        );
 294    });
 295    assert_eq!(
 296        mem::take(&mut *tree_updates.lock()),
 297        &[
 298            (rel_path("deps/dep-dir3").into(), PathChange::Loaded),
 299            (rel_path("deps/dep-dir3/deps").into(), PathChange::Loaded),
 300            (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded)
 301        ]
 302    );
 303
 304    // Expand a subdirectory of one of the symlinked directories.
 305    tree.read_with(cx, |tree, _| {
 306        tree.as_local()
 307            .unwrap()
 308            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3/src").into()])
 309    })
 310    .recv()
 311    .await;
 312
 313    // The expanded subdirectory's contents are loaded.
 314    tree.read_with(cx, |tree, _| {
 315        assert_eq!(
 316            tree.entries(true, 0)
 317                .map(|entry| (entry.path.as_ref(), entry.is_external))
 318                .collect::<Vec<_>>(),
 319            vec![
 320                (rel_path(""), false),
 321                (rel_path("deps"), false),
 322                (rel_path("deps/dep-dir2"), true),
 323                (rel_path("deps/dep-dir3"), true),
 324                (rel_path("deps/dep-dir3/deps"), true),
 325                (rel_path("deps/dep-dir3/src"), true),
 326                (rel_path("deps/dep-dir3/src/e.rs"), true),
 327                (rel_path("deps/dep-dir3/src/f.rs"), true),
 328                (rel_path("src"), false),
 329                (rel_path("src/a.rs"), false),
 330                (rel_path("src/b.rs"), false),
 331            ]
 332        );
 333    });
 334
 335    assert_eq!(
 336        mem::take(&mut *tree_updates.lock()),
 337        &[
 338            (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded),
 339            (
 340                rel_path("deps/dep-dir3/src/e.rs").into(),
 341                PathChange::Loaded
 342            ),
 343            (
 344                rel_path("deps/dep-dir3/src/f.rs").into(),
 345                PathChange::Loaded
 346            )
 347        ]
 348    );
 349}
 350
 351#[cfg(target_os = "macos")]
 352#[gpui::test]
 353async fn test_renaming_case_only(cx: &mut TestAppContext) {
 354    cx.executor().allow_parking();
 355    init_test(cx);
 356
 357    const OLD_NAME: &str = "aaa.rs";
 358    const NEW_NAME: &str = "AAA.rs";
 359
 360    let fs = Arc::new(RealFs::new(None, cx.executor()));
 361    let temp_root = TempTree::new(json!({
 362        OLD_NAME: "",
 363    }));
 364
 365    let tree = Worktree::local(
 366        temp_root.path(),
 367        true,
 368        fs.clone(),
 369        Default::default(),
 370        true,
 371        WorktreeId::from_proto(0),
 372        &mut cx.to_async(),
 373    )
 374    .await
 375    .unwrap();
 376
 377    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 378        .await;
 379    tree.read_with(cx, |tree, _| {
 380        assert_eq!(
 381            tree.entries(true, 0)
 382                .map(|entry| entry.path.as_ref())
 383                .collect::<Vec<_>>(),
 384            vec![rel_path(""), rel_path(OLD_NAME)]
 385        );
 386    });
 387
 388    fs.rename(
 389        &temp_root.path().join(OLD_NAME),
 390        &temp_root.path().join(NEW_NAME),
 391        fs::RenameOptions {
 392            overwrite: true,
 393            ignore_if_exists: true,
 394            create_parents: false,
 395        },
 396    )
 397    .await
 398    .unwrap();
 399
 400    tree.flush_fs_events(cx).await;
 401
 402    tree.read_with(cx, |tree, _| {
 403        assert_eq!(
 404            tree.entries(true, 0)
 405                .map(|entry| entry.path.as_ref())
 406                .collect::<Vec<_>>(),
 407            vec![rel_path(""), rel_path(NEW_NAME)]
 408        );
 409    });
 410}
 411
 412#[gpui::test]
 413async fn test_root_rescan_reconciles_stale_state(cx: &mut TestAppContext) {
 414    init_test(cx);
 415    let fs = FakeFs::new(cx.background_executor.clone());
 416    fs.insert_tree(
 417        "/root",
 418        json!({
 419            "old.txt": "",
 420        }),
 421    )
 422    .await;
 423
 424    let tree = Worktree::local(
 425        Path::new("/root"),
 426        true,
 427        fs.clone(),
 428        Default::default(),
 429        true,
 430        WorktreeId::from_proto(0),
 431        &mut cx.to_async(),
 432    )
 433    .await
 434    .unwrap();
 435
 436    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 437        .await;
 438
 439    tree.read_with(cx, |tree, _| {
 440        assert_eq!(
 441            tree.entries(true, 0)
 442                .map(|entry| entry.path.as_ref())
 443                .collect::<Vec<_>>(),
 444            vec![rel_path(""), rel_path("old.txt")]
 445        );
 446    });
 447
 448    fs.pause_events();
 449    fs.remove_file(Path::new("/root/old.txt"), RemoveOptions::default())
 450        .await
 451        .unwrap();
 452    fs.insert_file(Path::new("/root/new.txt"), Vec::new()).await;
 453    assert_eq!(fs.buffered_event_count(), 2);
 454    fs.clear_buffered_events();
 455
 456    tree.read_with(cx, |tree, _| {
 457        assert!(tree.entry_for_path(rel_path("old.txt")).is_some());
 458        assert!(tree.entry_for_path(rel_path("new.txt")).is_none());
 459    });
 460
 461    fs.emit_fs_event("/root", Some(fs::PathEventKind::Rescan));
 462    fs.unpause_events_and_flush();
 463    tree.flush_fs_events(cx).await;
 464
 465    tree.read_with(cx, |tree, _| {
 466        assert!(tree.entry_for_path(rel_path("old.txt")).is_none());
 467        assert!(tree.entry_for_path(rel_path("new.txt")).is_some());
 468        assert_eq!(
 469            tree.entries(true, 0)
 470                .map(|entry| entry.path.as_ref())
 471                .collect::<Vec<_>>(),
 472            vec![rel_path(""), rel_path("new.txt")]
 473        );
 474    });
 475}
 476
 477#[gpui::test]
 478async fn test_subtree_rescan_reports_unchanged_descendants_as_updated(cx: &mut TestAppContext) {
 479    init_test(cx);
 480    let fs = FakeFs::new(cx.background_executor.clone());
 481    fs.insert_tree(
 482        "/root",
 483        json!({
 484            "dir": {
 485                "child.txt": "",
 486                "nested": {
 487                    "grandchild.txt": "",
 488                },
 489                "remove": {
 490                    "removed.txt": "",
 491                }
 492            },
 493            "other.txt": "",
 494        }),
 495    )
 496    .await;
 497
 498    let tree = Worktree::local(
 499        Path::new("/root"),
 500        true,
 501        fs.clone(),
 502        Default::default(),
 503        true,
 504        WorktreeId::from_proto(0),
 505        &mut cx.to_async(),
 506    )
 507    .await
 508    .unwrap();
 509
 510    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 511        .await;
 512
 513    let tree_updates = Arc::new(Mutex::new(Vec::new()));
 514    tree.update(cx, |_, cx| {
 515        let tree_updates = tree_updates.clone();
 516        cx.subscribe(&tree, move |_, _, event, _| {
 517            if let Event::UpdatedEntries(update) = event {
 518                tree_updates.lock().extend(
 519                    update
 520                        .iter()
 521                        .filter(|(path, _, _)| path.as_ref() != rel_path("fs-event-sentinel"))
 522                        .map(|(path, _, change)| (path.clone(), *change)),
 523                );
 524            }
 525        })
 526        .detach();
 527    });
 528    fs.pause_events();
 529    fs.insert_file("/root/dir/new.txt", b"new content".to_vec())
 530        .await;
 531    fs.remove_dir(
 532        "/root/dir/remove".as_ref(),
 533        RemoveOptions {
 534            recursive: true,
 535            ignore_if_not_exists: false,
 536        },
 537    )
 538    .await
 539    .unwrap();
 540    fs.clear_buffered_events();
 541    fs.unpause_events_and_flush();
 542
 543    fs.emit_fs_event("/root/dir", Some(fs::PathEventKind::Rescan));
 544    tree.flush_fs_events(cx).await;
 545
 546    assert_eq!(
 547        mem::take(&mut *tree_updates.lock()),
 548        &[
 549            (rel_path("dir").into(), PathChange::Updated),
 550            (rel_path("dir/child.txt").into(), PathChange::Updated),
 551            (rel_path("dir/nested").into(), PathChange::Updated),
 552            (
 553                rel_path("dir/nested/grandchild.txt").into(),
 554                PathChange::Updated
 555            ),
 556            (rel_path("dir/new.txt").into(), PathChange::Added),
 557            (rel_path("dir/remove").into(), PathChange::Removed),
 558            (
 559                rel_path("dir/remove/removed.txt").into(),
 560                PathChange::Removed
 561            ),
 562        ]
 563    );
 564
 565    tree.read_with(cx, |tree, _| {
 566        assert!(tree.entry_for_path(rel_path("other.txt")).is_some());
 567    });
 568}
 569
 570#[gpui::test]
 571async fn test_open_gitignored_files(cx: &mut TestAppContext) {
 572    init_test(cx);
 573    let fs = FakeFs::new(cx.background_executor.clone());
 574    fs.insert_tree(
 575        "/root",
 576        json!({
 577            ".gitignore": "node_modules\n",
 578            "one": {
 579                "node_modules": {
 580                    "a": {
 581                        "a1.js": "a1",
 582                        "a2.js": "a2",
 583                    },
 584                    "b": {
 585                        "b1.js": "b1",
 586                        "b2.js": "b2",
 587                    },
 588                    "c": {
 589                        "c1.js": "c1",
 590                        "c2.js": "c2",
 591                    }
 592                },
 593            },
 594            "two": {
 595                "x.js": "",
 596                "y.js": "",
 597            },
 598        }),
 599    )
 600    .await;
 601
 602    let tree = Worktree::local(
 603        Path::new("/root"),
 604        true,
 605        fs.clone(),
 606        Default::default(),
 607        true,
 608        WorktreeId::from_proto(0),
 609        &mut cx.to_async(),
 610    )
 611    .await
 612    .unwrap();
 613
 614    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 615        .await;
 616
 617    tree.read_with(cx, |tree, _| {
 618        assert_eq!(
 619            tree.entries(true, 0)
 620                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 621                .collect::<Vec<_>>(),
 622            vec![
 623                (rel_path(""), false),
 624                (rel_path(".gitignore"), false),
 625                (rel_path("one"), false),
 626                (rel_path("one/node_modules"), true),
 627                (rel_path("two"), false),
 628                (rel_path("two/x.js"), false),
 629                (rel_path("two/y.js"), false),
 630            ]
 631        );
 632    });
 633
 634    // Open a file that is nested inside of a gitignored directory that
 635    // has not yet been expanded.
 636    let prev_read_dir_count = fs.read_dir_call_count();
 637    let loaded = tree
 638        .update(cx, |tree, cx| {
 639            tree.load_file(rel_path("one/node_modules/b/b1.js"), cx)
 640        })
 641        .await
 642        .unwrap();
 643
 644    tree.read_with(cx, |tree, _| {
 645        assert_eq!(
 646            tree.entries(true, 0)
 647                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 648                .collect::<Vec<_>>(),
 649            vec![
 650                (rel_path(""), false),
 651                (rel_path(".gitignore"), false),
 652                (rel_path("one"), false),
 653                (rel_path("one/node_modules"), true),
 654                (rel_path("one/node_modules/a"), true),
 655                (rel_path("one/node_modules/b"), true),
 656                (rel_path("one/node_modules/b/b1.js"), true),
 657                (rel_path("one/node_modules/b/b2.js"), true),
 658                (rel_path("one/node_modules/c"), true),
 659                (rel_path("two"), false),
 660                (rel_path("two/x.js"), false),
 661                (rel_path("two/y.js"), false),
 662            ]
 663        );
 664
 665        assert_eq!(
 666            loaded.file.path.as_ref(),
 667            rel_path("one/node_modules/b/b1.js")
 668        );
 669
 670        // Only the newly-expanded directories are scanned.
 671        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
 672    });
 673
 674    // Open another file in a different subdirectory of the same
 675    // gitignored directory.
 676    let prev_read_dir_count = fs.read_dir_call_count();
 677    let loaded = tree
 678        .update(cx, |tree, cx| {
 679            tree.load_file(rel_path("one/node_modules/a/a2.js"), cx)
 680        })
 681        .await
 682        .unwrap();
 683
 684    tree.read_with(cx, |tree, _| {
 685        assert_eq!(
 686            tree.entries(true, 0)
 687                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 688                .collect::<Vec<_>>(),
 689            vec![
 690                (rel_path(""), false),
 691                (rel_path(".gitignore"), false),
 692                (rel_path("one"), false),
 693                (rel_path("one/node_modules"), true),
 694                (rel_path("one/node_modules/a"), true),
 695                (rel_path("one/node_modules/a/a1.js"), true),
 696                (rel_path("one/node_modules/a/a2.js"), true),
 697                (rel_path("one/node_modules/b"), true),
 698                (rel_path("one/node_modules/b/b1.js"), true),
 699                (rel_path("one/node_modules/b/b2.js"), true),
 700                (rel_path("one/node_modules/c"), true),
 701                (rel_path("two"), false),
 702                (rel_path("two/x.js"), false),
 703                (rel_path("two/y.js"), false),
 704            ]
 705        );
 706
 707        assert_eq!(
 708            loaded.file.path.as_ref(),
 709            rel_path("one/node_modules/a/a2.js")
 710        );
 711
 712        // Only the newly-expanded directory is scanned.
 713        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
 714    });
 715
 716    let path = PathBuf::from("/root/one/node_modules/c/lib");
 717
 718    // No work happens when files and directories change within an unloaded directory.
 719    let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
 720    // When we open a directory, we check each ancestor whether it's a git
 721    // repository. That means we have an fs.metadata call per ancestor that we
 722    // need to subtract here.
 723    let ancestors = path.ancestors().count();
 724
 725    fs.create_dir(path.as_ref()).await.unwrap();
 726    cx.executor().run_until_parked();
 727
 728    assert_eq!(
 729        fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count - ancestors,
 730        0
 731    );
 732}
 733
 734#[gpui::test]
 735async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
 736    init_test(cx);
 737    let fs = FakeFs::new(cx.background_executor.clone());
 738    fs.insert_tree(
 739        "/root",
 740        json!({
 741            ".gitignore": "node_modules\n",
 742            "a": {
 743                "a.js": "",
 744            },
 745            "b": {
 746                "b.js": "",
 747            },
 748            "node_modules": {
 749                "c": {
 750                    "c.js": "",
 751                },
 752                "d": {
 753                    "d.js": "",
 754                    "e": {
 755                        "e1.js": "",
 756                        "e2.js": "",
 757                    },
 758                    "f": {
 759                        "f1.js": "",
 760                        "f2.js": "",
 761                    }
 762                },
 763            },
 764        }),
 765    )
 766    .await;
 767
 768    let tree = Worktree::local(
 769        Path::new("/root"),
 770        true,
 771        fs.clone(),
 772        Default::default(),
 773        true,
 774        WorktreeId::from_proto(0),
 775        &mut cx.to_async(),
 776    )
 777    .await
 778    .unwrap();
 779
 780    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 781        .await;
 782
 783    // Open a file within the gitignored directory, forcing some of its
 784    // subdirectories to be read, but not all.
 785    let read_dir_count_1 = fs.read_dir_call_count();
 786    tree.read_with(cx, |tree, _| {
 787        tree.as_local()
 788            .unwrap()
 789            .refresh_entries_for_paths(vec![rel_path("node_modules/d/d.js").into()])
 790    })
 791    .recv()
 792    .await;
 793
 794    // Those subdirectories are now loaded.
 795    tree.read_with(cx, |tree, _| {
 796        assert_eq!(
 797            tree.entries(true, 0)
 798                .map(|e| (e.path.as_ref(), e.is_ignored))
 799                .collect::<Vec<_>>(),
 800            &[
 801                (rel_path(""), false),
 802                (rel_path(".gitignore"), false),
 803                (rel_path("a"), false),
 804                (rel_path("a/a.js"), false),
 805                (rel_path("b"), false),
 806                (rel_path("b/b.js"), false),
 807                (rel_path("node_modules"), true),
 808                (rel_path("node_modules/c"), true),
 809                (rel_path("node_modules/d"), true),
 810                (rel_path("node_modules/d/d.js"), true),
 811                (rel_path("node_modules/d/e"), true),
 812                (rel_path("node_modules/d/f"), true),
 813            ]
 814        );
 815    });
 816    let read_dir_count_2 = fs.read_dir_call_count();
 817    assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
 818
 819    // Update the gitignore so that node_modules is no longer ignored,
 820    // but a subdirectory is ignored
 821    fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
 822        .await
 823        .unwrap();
 824    cx.executor().run_until_parked();
 825
 826    // All of the directories that are no longer ignored are now loaded.
 827    tree.read_with(cx, |tree, _| {
 828        assert_eq!(
 829            tree.entries(true, 0)
 830                .map(|e| (e.path.as_ref(), e.is_ignored))
 831                .collect::<Vec<_>>(),
 832            &[
 833                (rel_path(""), false),
 834                (rel_path(".gitignore"), false),
 835                (rel_path("a"), false),
 836                (rel_path("a/a.js"), false),
 837                (rel_path("b"), false),
 838                (rel_path("b/b.js"), false),
 839                // This directory is no longer ignored
 840                (rel_path("node_modules"), false),
 841                (rel_path("node_modules/c"), false),
 842                (rel_path("node_modules/c/c.js"), false),
 843                (rel_path("node_modules/d"), false),
 844                (rel_path("node_modules/d/d.js"), false),
 845                // This subdirectory is now ignored
 846                (rel_path("node_modules/d/e"), true),
 847                (rel_path("node_modules/d/f"), false),
 848                (rel_path("node_modules/d/f/f1.js"), false),
 849                (rel_path("node_modules/d/f/f2.js"), false),
 850            ]
 851        );
 852    });
 853
 854    // Each of the newly-loaded directories is scanned only once.
 855    let read_dir_count_3 = fs.read_dir_call_count();
 856    assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
 857}
 858
 859#[gpui::test]
 860async fn test_write_file(cx: &mut TestAppContext) {
 861    init_test(cx);
 862    cx.executor().allow_parking();
 863    let dir = TempTree::new(json!({
 864        ".git": {},
 865        ".gitignore": "ignored-dir\n",
 866        "tracked-dir": {},
 867        "ignored-dir": {}
 868    }));
 869
 870    let worktree = Worktree::local(
 871        dir.path(),
 872        true,
 873        Arc::new(RealFs::new(None, cx.executor())),
 874        Default::default(),
 875        true,
 876        WorktreeId::from_proto(0),
 877        &mut cx.to_async(),
 878    )
 879    .await
 880    .unwrap();
 881
 882    #[cfg(not(target_os = "macos"))]
 883    fs::fs_watcher::global(|_| {}).unwrap();
 884
 885    cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
 886        .await;
 887    worktree.flush_fs_events(cx).await;
 888
 889    worktree
 890        .update(cx, |tree, cx| {
 891            tree.write_file(
 892                rel_path("tracked-dir/file.txt").into(),
 893                "hello".into(),
 894                Default::default(),
 895                encoding_rs::UTF_8,
 896                false,
 897                cx,
 898            )
 899        })
 900        .await
 901        .unwrap();
 902    worktree
 903        .update(cx, |tree, cx| {
 904            tree.write_file(
 905                rel_path("ignored-dir/file.txt").into(),
 906                "world".into(),
 907                Default::default(),
 908                encoding_rs::UTF_8,
 909                false,
 910                cx,
 911            )
 912        })
 913        .await
 914        .unwrap();
 915    worktree.read_with(cx, |tree, _| {
 916        let tracked = tree
 917            .entry_for_path(rel_path("tracked-dir/file.txt"))
 918            .unwrap();
 919        let ignored = tree
 920            .entry_for_path(rel_path("ignored-dir/file.txt"))
 921            .unwrap();
 922        assert!(!tracked.is_ignored);
 923        assert!(ignored.is_ignored);
 924    });
 925}
 926
 927#[gpui::test]
 928async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
 929    init_test(cx);
 930    cx.executor().allow_parking();
 931    let dir = TempTree::new(json!({
 932        ".gitignore": "**/target\n/node_modules\ntop_level.txt\n",
 933        "target": {
 934            "index": "blah2"
 935        },
 936        "node_modules": {
 937            ".DS_Store": "",
 938            "prettier": {
 939                "package.json": "{}",
 940            },
 941            "package.json": "//package.json"
 942        },
 943        "src": {
 944            ".DS_Store": "",
 945            "foo": {
 946                "foo.rs": "mod another;\n",
 947                "another.rs": "// another",
 948            },
 949            "bar": {
 950                "bar.rs": "// bar",
 951            },
 952            "lib.rs": "mod foo;\nmod bar;\n",
 953        },
 954        "top_level.txt": "top level file",
 955        ".DS_Store": "",
 956    }));
 957    cx.update(|cx| {
 958        cx.update_global::<SettingsStore, _>(|store, cx| {
 959            store.update_user_settings(cx, |settings| {
 960                settings.project.worktree.file_scan_exclusions = Some(vec![]);
 961                settings.project.worktree.file_scan_inclusions = Some(vec![
 962                    "node_modules/**/package.json".to_string(),
 963                    "**/.DS_Store".to_string(),
 964                ]);
 965            });
 966        });
 967    });
 968
 969    let tree = Worktree::local(
 970        dir.path(),
 971        true,
 972        Arc::new(RealFs::new(None, cx.executor())),
 973        Default::default(),
 974        true,
 975        WorktreeId::from_proto(0),
 976        &mut cx.to_async(),
 977    )
 978    .await
 979    .unwrap();
 980    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 981        .await;
 982    tree.flush_fs_events(cx).await;
 983    tree.read_with(cx, |tree, _| {
 984        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
 985        check_worktree_entries(
 986            tree,
 987            &[],
 988            &["target", "node_modules"],
 989            &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
 990            &[
 991                "node_modules/prettier/package.json",
 992                ".DS_Store",
 993                "node_modules/.DS_Store",
 994                "src/.DS_Store",
 995            ],
 996        )
 997    });
 998}
 999
1000#[gpui::test]
1001async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) {
1002    init_test(cx);
1003    cx.executor().allow_parking();
1004    let dir = TempTree::new(json!({
1005        ".gitignore": "**/target\n/node_modules\n",
1006        "target": {
1007            "index": "blah2"
1008        },
1009        "node_modules": {
1010            ".DS_Store": "",
1011            "prettier": {
1012                "package.json": "{}",
1013            },
1014        },
1015        "src": {
1016            ".DS_Store": "",
1017            "foo": {
1018                "foo.rs": "mod another;\n",
1019                "another.rs": "// another",
1020            },
1021        },
1022        ".DS_Store": "",
1023    }));
1024
1025    cx.update(|cx| {
1026        cx.update_global::<SettingsStore, _>(|store, cx| {
1027            store.update_user_settings(cx, |settings| {
1028                settings.project.worktree.file_scan_exclusions =
1029                    Some(vec!["**/.DS_Store".to_string()]);
1030                settings.project.worktree.file_scan_inclusions =
1031                    Some(vec!["**/.DS_Store".to_string()]);
1032            });
1033        });
1034    });
1035
1036    let tree = Worktree::local(
1037        dir.path(),
1038        true,
1039        Arc::new(RealFs::new(None, cx.executor())),
1040        Default::default(),
1041        true,
1042        WorktreeId::from_proto(0),
1043        &mut cx.to_async(),
1044    )
1045    .await
1046    .unwrap();
1047    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1048        .await;
1049    tree.flush_fs_events(cx).await;
1050    tree.read_with(cx, |tree, _| {
1051        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
1052        check_worktree_entries(
1053            tree,
1054            &[".DS_Store, src/.DS_Store"],
1055            &["target", "node_modules"],
1056            &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"],
1057            &[],
1058        )
1059    });
1060}
1061
1062#[gpui::test]
1063async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) {
1064    init_test(cx);
1065    cx.executor().allow_parking();
1066    let dir = TempTree::new(json!({
1067        ".gitignore": "**/target\n/node_modules/\n",
1068        "target": {
1069            "index": "blah2"
1070        },
1071        "node_modules": {
1072            ".DS_Store": "",
1073            "prettier": {
1074                "package.json": "{}",
1075            },
1076        },
1077        "src": {
1078            ".DS_Store": "",
1079            "foo": {
1080                "foo.rs": "mod another;\n",
1081                "another.rs": "// another",
1082            },
1083        },
1084        ".DS_Store": "",
1085    }));
1086
1087    cx.update(|cx| {
1088        cx.update_global::<SettingsStore, _>(|store, cx| {
1089            store.update_user_settings(cx, |settings| {
1090                settings.project.worktree.file_scan_exclusions = Some(vec![]);
1091                settings.project.worktree.file_scan_inclusions =
1092                    Some(vec!["node_modules/**".to_string()]);
1093            });
1094        });
1095    });
1096    let tree = Worktree::local(
1097        dir.path(),
1098        true,
1099        Arc::new(RealFs::new(None, cx.executor())),
1100        Default::default(),
1101        true,
1102        WorktreeId::from_proto(0),
1103        &mut cx.to_async(),
1104    )
1105    .await
1106    .unwrap();
1107    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1108        .await;
1109    tree.flush_fs_events(cx).await;
1110
1111    tree.read_with(cx, |tree, _| {
1112        assert!(
1113            tree.entry_for_path(rel_path("node_modules"))
1114                .is_some_and(|f| f.is_always_included)
1115        );
1116        assert!(
1117            tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
1118                .is_some_and(|f| f.is_always_included)
1119        );
1120    });
1121
1122    cx.update(|cx| {
1123        cx.update_global::<SettingsStore, _>(|store, cx| {
1124            store.update_user_settings(cx, |settings| {
1125                settings.project.worktree.file_scan_exclusions = Some(vec![]);
1126                settings.project.worktree.file_scan_inclusions = Some(vec![]);
1127            });
1128        });
1129    });
1130    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1131        .await;
1132    tree.flush_fs_events(cx).await;
1133
1134    tree.read_with(cx, |tree, _| {
1135        assert!(
1136            tree.entry_for_path(rel_path("node_modules"))
1137                .is_some_and(|f| !f.is_always_included)
1138        );
1139        assert!(
1140            tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
1141                .is_some_and(|f| !f.is_always_included)
1142        );
1143    });
1144}
1145
1146#[gpui::test]
1147async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
1148    init_test(cx);
1149    cx.executor().allow_parking();
1150    let dir = TempTree::new(json!({
1151        ".gitignore": "**/target\n/node_modules\n",
1152        "target": {
1153            "index": "blah2"
1154        },
1155        "node_modules": {
1156            ".DS_Store": "",
1157            "prettier": {
1158                "package.json": "{}",
1159            },
1160        },
1161        "src": {
1162            ".DS_Store": "",
1163            "foo": {
1164                "foo.rs": "mod another;\n",
1165                "another.rs": "// another",
1166            },
1167            "bar": {
1168                "bar.rs": "// bar",
1169            },
1170            "lib.rs": "mod foo;\nmod bar;\n",
1171        },
1172        ".DS_Store": "",
1173    }));
1174    cx.update(|cx| {
1175        cx.update_global::<SettingsStore, _>(|store, cx| {
1176            store.update_user_settings(cx, |settings| {
1177                settings.project.worktree.file_scan_exclusions =
1178                    Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
1179            });
1180        });
1181    });
1182
1183    let tree = Worktree::local(
1184        dir.path(),
1185        true,
1186        Arc::new(RealFs::new(None, cx.executor())),
1187        Default::default(),
1188        true,
1189        WorktreeId::from_proto(0),
1190        &mut cx.to_async(),
1191    )
1192    .await
1193    .unwrap();
1194    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1195        .await;
1196    tree.flush_fs_events(cx).await;
1197    tree.read_with(cx, |tree, _| {
1198        check_worktree_entries(
1199            tree,
1200            &[
1201                "src/foo/foo.rs",
1202                "src/foo/another.rs",
1203                "node_modules/.DS_Store",
1204                "src/.DS_Store",
1205                ".DS_Store",
1206            ],
1207            &["target", "node_modules"],
1208            &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
1209            &[],
1210        )
1211    });
1212
1213    cx.update(|cx| {
1214        cx.update_global::<SettingsStore, _>(|store, cx| {
1215            store.update_user_settings(cx, |settings| {
1216                settings.project.worktree.file_scan_exclusions =
1217                    Some(vec!["**/node_modules/**".to_string()]);
1218            });
1219        });
1220    });
1221    tree.flush_fs_events(cx).await;
1222    cx.executor().run_until_parked();
1223    tree.read_with(cx, |tree, _| {
1224        check_worktree_entries(
1225            tree,
1226            &[
1227                "node_modules/prettier/package.json",
1228                "node_modules/.DS_Store",
1229                "node_modules",
1230            ],
1231            &["target"],
1232            &[
1233                ".gitignore",
1234                "src/lib.rs",
1235                "src/bar/bar.rs",
1236                "src/foo/foo.rs",
1237                "src/foo/another.rs",
1238                "src/.DS_Store",
1239                ".DS_Store",
1240            ],
1241            &[],
1242        )
1243    });
1244}
1245
1246#[gpui::test]
1247async fn test_hidden_files(cx: &mut TestAppContext) {
1248    init_test(cx);
1249    cx.executor().allow_parking();
1250    let dir = TempTree::new(json!({
1251        ".gitignore": "**/target\n",
1252        ".hidden_file": "content",
1253        ".hidden_dir": {
1254            "nested.rs": "code",
1255        },
1256        "src": {
1257            "visible.rs": "code",
1258        },
1259        "logs": {
1260            "app.log": "logs",
1261            "debug.log": "logs",
1262        },
1263        "visible.txt": "content",
1264    }));
1265
1266    let tree = Worktree::local(
1267        dir.path(),
1268        true,
1269        Arc::new(RealFs::new(None, cx.executor())),
1270        Default::default(),
1271        true,
1272        WorktreeId::from_proto(0),
1273        &mut cx.to_async(),
1274    )
1275    .await
1276    .unwrap();
1277    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1278        .await;
1279    tree.flush_fs_events(cx).await;
1280
1281    tree.read_with(cx, |tree, _| {
1282        assert_eq!(
1283            tree.entries(true, 0)
1284                .map(|entry| (entry.path.as_ref(), entry.is_hidden))
1285                .collect::<Vec<_>>(),
1286            vec![
1287                (rel_path(""), false),
1288                (rel_path(".gitignore"), true),
1289                (rel_path(".hidden_dir"), true),
1290                (rel_path(".hidden_dir/nested.rs"), true),
1291                (rel_path(".hidden_file"), true),
1292                (rel_path("logs"), false),
1293                (rel_path("logs/app.log"), false),
1294                (rel_path("logs/debug.log"), false),
1295                (rel_path("src"), false),
1296                (rel_path("src/visible.rs"), false),
1297                (rel_path("visible.txt"), false),
1298            ]
1299        );
1300    });
1301
1302    cx.update(|cx| {
1303        cx.update_global::<SettingsStore, _>(|store, cx| {
1304            store.update_user_settings(cx, |settings| {
1305                settings.project.worktree.hidden_files = Some(vec!["**/*.log".to_string()]);
1306            });
1307        });
1308    });
1309    tree.flush_fs_events(cx).await;
1310    cx.executor().run_until_parked();
1311
1312    tree.read_with(cx, |tree, _| {
1313        assert_eq!(
1314            tree.entries(true, 0)
1315                .map(|entry| (entry.path.as_ref(), entry.is_hidden))
1316                .collect::<Vec<_>>(),
1317            vec![
1318                (rel_path(""), false),
1319                (rel_path(".gitignore"), false),
1320                (rel_path(".hidden_dir"), false),
1321                (rel_path(".hidden_dir/nested.rs"), false),
1322                (rel_path(".hidden_file"), false),
1323                (rel_path("logs"), false),
1324                (rel_path("logs/app.log"), true),
1325                (rel_path("logs/debug.log"), true),
1326                (rel_path("src"), false),
1327                (rel_path("src/visible.rs"), false),
1328                (rel_path("visible.txt"), false),
1329            ]
1330        );
1331    });
1332}
1333
1334#[gpui::test]
1335async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
1336    init_test(cx);
1337    cx.executor().allow_parking();
1338    let dir = TempTree::new(json!({
1339        ".git": {
1340            "HEAD": "ref: refs/heads/main\n",
1341            "foo": "bar",
1342        },
1343        ".gitignore": "**/target\n/node_modules\ntest_output\n",
1344        "target": {
1345            "index": "blah2"
1346        },
1347        "node_modules": {
1348            ".DS_Store": "",
1349            "prettier": {
1350                "package.json": "{}",
1351            },
1352        },
1353        "src": {
1354            ".DS_Store": "",
1355            "foo": {
1356                "foo.rs": "mod another;\n",
1357                "another.rs": "// another",
1358            },
1359            "bar": {
1360                "bar.rs": "// bar",
1361            },
1362            "lib.rs": "mod foo;\nmod bar;\n",
1363        },
1364        ".DS_Store": "",
1365    }));
1366    cx.update(|cx| {
1367        cx.update_global::<SettingsStore, _>(|store, cx| {
1368            store.update_user_settings(cx, |settings| {
1369                settings.project.worktree.file_scan_exclusions = Some(vec![
1370                    "**/.git".to_string(),
1371                    "node_modules/".to_string(),
1372                    "build_output".to_string(),
1373                ]);
1374            });
1375        });
1376    });
1377
1378    let tree = Worktree::local(
1379        dir.path(),
1380        true,
1381        Arc::new(RealFs::new(None, cx.executor())),
1382        Default::default(),
1383        true,
1384        WorktreeId::from_proto(0),
1385        &mut cx.to_async(),
1386    )
1387    .await
1388    .unwrap();
1389    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1390        .await;
1391    tree.flush_fs_events(cx).await;
1392    tree.read_with(cx, |tree, _| {
1393        check_worktree_entries(
1394            tree,
1395            &[
1396                ".git/HEAD",
1397                ".git/foo",
1398                "node_modules",
1399                "node_modules/.DS_Store",
1400                "node_modules/prettier",
1401                "node_modules/prettier/package.json",
1402            ],
1403            &["target"],
1404            &[
1405                ".DS_Store",
1406                "src/.DS_Store",
1407                "src/lib.rs",
1408                "src/foo/foo.rs",
1409                "src/foo/another.rs",
1410                "src/bar/bar.rs",
1411                ".gitignore",
1412            ],
1413            &[],
1414        )
1415    });
1416
1417    let new_excluded_dir = dir.path().join("build_output");
1418    let new_ignored_dir = dir.path().join("test_output");
1419    std::fs::create_dir_all(&new_excluded_dir)
1420        .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1421    std::fs::create_dir_all(&new_ignored_dir)
1422        .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1423    let node_modules_dir = dir.path().join("node_modules");
1424    let dot_git_dir = dir.path().join(".git");
1425    let src_dir = dir.path().join("src");
1426    for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1427        assert!(
1428            existing_dir.is_dir(),
1429            "Expect {existing_dir:?} to be present in the FS already"
1430        );
1431    }
1432
1433    for directory_for_new_file in [
1434        new_excluded_dir,
1435        new_ignored_dir,
1436        node_modules_dir,
1437        dot_git_dir,
1438        src_dir,
1439    ] {
1440        std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1441            .unwrap_or_else(|e| {
1442                panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1443            });
1444    }
1445    tree.flush_fs_events(cx).await;
1446
1447    tree.read_with(cx, |tree, _| {
1448        check_worktree_entries(
1449            tree,
1450            &[
1451                ".git/HEAD",
1452                ".git/foo",
1453                ".git/new_file",
1454                "node_modules",
1455                "node_modules/.DS_Store",
1456                "node_modules/prettier",
1457                "node_modules/prettier/package.json",
1458                "node_modules/new_file",
1459                "build_output",
1460                "build_output/new_file",
1461                "test_output/new_file",
1462            ],
1463            &["target", "test_output"],
1464            &[
1465                ".DS_Store",
1466                "src/.DS_Store",
1467                "src/lib.rs",
1468                "src/foo/foo.rs",
1469                "src/foo/another.rs",
1470                "src/bar/bar.rs",
1471                "src/new_file",
1472                ".gitignore",
1473            ],
1474            &[],
1475        )
1476    });
1477}
1478
1479#[gpui::test]
1480async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1481    init_test(cx);
1482    cx.executor().allow_parking();
1483    let dir = TempTree::new(json!({
1484        ".git": {
1485            "HEAD": "ref: refs/heads/main\n",
1486            "foo": "foo contents",
1487        },
1488    }));
1489    let dot_git_worktree_dir = dir.path().join(".git");
1490
1491    let tree = Worktree::local(
1492        dot_git_worktree_dir.clone(),
1493        true,
1494        Arc::new(RealFs::new(None, cx.executor())),
1495        Default::default(),
1496        true,
1497        WorktreeId::from_proto(0),
1498        &mut cx.to_async(),
1499    )
1500    .await
1501    .unwrap();
1502    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1503        .await;
1504    tree.flush_fs_events(cx).await;
1505    tree.read_with(cx, |tree, _| {
1506        check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[])
1507    });
1508
1509    std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1510        .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1511    tree.flush_fs_events(cx).await;
1512    tree.read_with(cx, |tree, _| {
1513        check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[])
1514    });
1515}
1516
1517#[gpui::test(iterations = 30)]
1518async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1519    init_test(cx);
1520    let fs = FakeFs::new(cx.background_executor.clone());
1521    fs.insert_tree(
1522        "/root",
1523        json!({
1524            "b": {},
1525            "c": {},
1526            "d": {},
1527        }),
1528    )
1529    .await;
1530
1531    let tree = Worktree::local(
1532        "/root".as_ref(),
1533        true,
1534        fs,
1535        Default::default(),
1536        true,
1537        WorktreeId::from_proto(0),
1538        &mut cx.to_async(),
1539    )
1540    .await
1541    .unwrap();
1542
1543    let snapshot1 = tree.update(cx, |tree, cx| {
1544        let tree = tree.as_local_mut().unwrap();
1545        let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1546        tree.observe_updates(0, cx, {
1547            let snapshot = snapshot.clone();
1548            let settings = tree.settings();
1549            move |update| {
1550                snapshot
1551                    .lock()
1552                    .apply_remote_update(update, &settings.file_scan_inclusions);
1553                async { true }
1554            }
1555        });
1556        snapshot
1557    });
1558
1559    let entry = tree
1560        .update(cx, |tree, cx| {
1561            tree.as_local_mut()
1562                .unwrap()
1563                .create_entry(rel_path("a/e").into(), true, None, cx)
1564        })
1565        .await
1566        .unwrap()
1567        .into_included()
1568        .unwrap();
1569    assert!(entry.is_dir());
1570
1571    cx.executor().run_until_parked();
1572    tree.read_with(cx, |tree, _| {
1573        assert_eq!(
1574            tree.entry_for_path(rel_path("a/e")).unwrap().kind,
1575            EntryKind::Dir
1576        );
1577    });
1578
1579    let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1580    assert_eq!(
1581        snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1582        snapshot2.entries(true, 0).collect::<Vec<_>>()
1583    );
1584}
1585
1586#[gpui::test]
1587async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1588    init_test(cx);
1589    cx.executor().allow_parking();
1590
1591    let fs_fake = FakeFs::new(cx.background_executor.clone());
1592    fs_fake
1593        .insert_tree(
1594            "/root",
1595            json!({
1596                "a": {},
1597            }),
1598        )
1599        .await;
1600
1601    let tree_fake = Worktree::local(
1602        "/root".as_ref(),
1603        true,
1604        fs_fake,
1605        Default::default(),
1606        true,
1607        WorktreeId::from_proto(0),
1608        &mut cx.to_async(),
1609    )
1610    .await
1611    .unwrap();
1612
1613    let entry = tree_fake
1614        .update(cx, |tree, cx| {
1615            tree.as_local_mut().unwrap().create_entry(
1616                rel_path("a/b/c/d.txt").into(),
1617                false,
1618                None,
1619                cx,
1620            )
1621        })
1622        .await
1623        .unwrap()
1624        .into_included()
1625        .unwrap();
1626    assert!(entry.is_file());
1627
1628    cx.executor().run_until_parked();
1629    tree_fake.read_with(cx, |tree, _| {
1630        assert!(
1631            tree.entry_for_path(rel_path("a/b/c/d.txt"))
1632                .unwrap()
1633                .is_file()
1634        );
1635        assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
1636        assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
1637    });
1638
1639    let fs_real = Arc::new(RealFs::new(None, cx.executor()));
1640    let temp_root = TempTree::new(json!({
1641        "a": {}
1642    }));
1643
1644    let tree_real = Worktree::local(
1645        temp_root.path(),
1646        true,
1647        fs_real,
1648        Default::default(),
1649        true,
1650        WorktreeId::from_proto(0),
1651        &mut cx.to_async(),
1652    )
1653    .await
1654    .unwrap();
1655
1656    let entry = tree_real
1657        .update(cx, |tree, cx| {
1658            tree.as_local_mut().unwrap().create_entry(
1659                rel_path("a/b/c/d.txt").into(),
1660                false,
1661                None,
1662                cx,
1663            )
1664        })
1665        .await
1666        .unwrap()
1667        .into_included()
1668        .unwrap();
1669    assert!(entry.is_file());
1670
1671    cx.executor().run_until_parked();
1672    tree_real.read_with(cx, |tree, _| {
1673        assert!(
1674            tree.entry_for_path(rel_path("a/b/c/d.txt"))
1675                .unwrap()
1676                .is_file()
1677        );
1678        assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
1679        assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
1680    });
1681
1682    // Test smallest change
1683    let entry = tree_real
1684        .update(cx, |tree, cx| {
1685            tree.as_local_mut().unwrap().create_entry(
1686                rel_path("a/b/c/e.txt").into(),
1687                false,
1688                None,
1689                cx,
1690            )
1691        })
1692        .await
1693        .unwrap()
1694        .into_included()
1695        .unwrap();
1696    assert!(entry.is_file());
1697
1698    cx.executor().run_until_parked();
1699    tree_real.read_with(cx, |tree, _| {
1700        assert!(
1701            tree.entry_for_path(rel_path("a/b/c/e.txt"))
1702                .unwrap()
1703                .is_file()
1704        );
1705    });
1706
1707    // Test largest change
1708    let entry = tree_real
1709        .update(cx, |tree, cx| {
1710            tree.as_local_mut().unwrap().create_entry(
1711                rel_path("d/e/f/g.txt").into(),
1712                false,
1713                None,
1714                cx,
1715            )
1716        })
1717        .await
1718        .unwrap()
1719        .into_included()
1720        .unwrap();
1721    assert!(entry.is_file());
1722
1723    cx.executor().run_until_parked();
1724    tree_real.read_with(cx, |tree, _| {
1725        assert!(
1726            tree.entry_for_path(rel_path("d/e/f/g.txt"))
1727                .unwrap()
1728                .is_file()
1729        );
1730        assert!(tree.entry_for_path(rel_path("d/e/f")).unwrap().is_dir());
1731        assert!(tree.entry_for_path(rel_path("d/e")).unwrap().is_dir());
1732        assert!(tree.entry_for_path(rel_path("d")).unwrap().is_dir());
1733    });
1734}
1735
1736#[gpui::test]
1737async fn test_create_file_in_expanded_gitignored_dir(cx: &mut TestAppContext) {
1738    // Tests the behavior of our worktree refresh when a file in a gitignored directory
1739    // is created.
1740    init_test(cx);
1741    let fs = FakeFs::new(cx.background_executor.clone());
1742    fs.insert_tree(
1743        "/root",
1744        json!({
1745            ".gitignore": "ignored_dir\n",
1746            "ignored_dir": {
1747                "existing_file.txt": "existing content",
1748                "another_file.txt": "another content",
1749            },
1750        }),
1751    )
1752    .await;
1753
1754    let tree = Worktree::local(
1755        Path::new("/root"),
1756        true,
1757        fs.clone(),
1758        Default::default(),
1759        true,
1760        WorktreeId::from_proto(0),
1761        &mut cx.to_async(),
1762    )
1763    .await
1764    .unwrap();
1765
1766    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1767        .await;
1768
1769    tree.read_with(cx, |tree, _| {
1770        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
1771        assert!(ignored_dir.is_ignored);
1772        assert_eq!(ignored_dir.kind, EntryKind::UnloadedDir);
1773    });
1774
1775    tree.update(cx, |tree, cx| {
1776        tree.load_file(rel_path("ignored_dir/existing_file.txt"), cx)
1777    })
1778    .await
1779    .unwrap();
1780
1781    tree.read_with(cx, |tree, _| {
1782        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
1783        assert!(ignored_dir.is_ignored);
1784        assert_eq!(ignored_dir.kind, EntryKind::Dir);
1785
1786        assert!(
1787            tree.entry_for_path(rel_path("ignored_dir/existing_file.txt"))
1788                .is_some()
1789        );
1790        assert!(
1791            tree.entry_for_path(rel_path("ignored_dir/another_file.txt"))
1792                .is_some()
1793        );
1794    });
1795
1796    let entry = tree
1797        .update(cx, |tree, cx| {
1798            tree.create_entry(rel_path("ignored_dir/new_file.txt").into(), false, None, cx)
1799        })
1800        .await
1801        .unwrap();
1802    assert!(entry.into_included().is_some());
1803
1804    cx.executor().run_until_parked();
1805
1806    tree.read_with(cx, |tree, _| {
1807        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
1808        assert!(ignored_dir.is_ignored);
1809        assert_eq!(
1810            ignored_dir.kind,
1811            EntryKind::Dir,
1812            "ignored_dir should still be loaded, not UnloadedDir"
1813        );
1814
1815        assert!(
1816            tree.entry_for_path(rel_path("ignored_dir/existing_file.txt"))
1817                .is_some(),
1818            "existing_file.txt should still be visible"
1819        );
1820        assert!(
1821            tree.entry_for_path(rel_path("ignored_dir/another_file.txt"))
1822                .is_some(),
1823            "another_file.txt should still be visible"
1824        );
1825        assert!(
1826            tree.entry_for_path(rel_path("ignored_dir/new_file.txt"))
1827                .is_some(),
1828            "new_file.txt should be visible"
1829        );
1830    });
1831}
1832
1833#[gpui::test]
1834async fn test_fs_event_for_gitignored_dir_does_not_lose_contents(cx: &mut TestAppContext) {
1835    // Tests the behavior of our worktree refresh when a directory modification for a gitignored directory
1836    // is triggered.
1837    init_test(cx);
1838    let fs = FakeFs::new(cx.background_executor.clone());
1839    fs.insert_tree(
1840        "/root",
1841        json!({
1842            ".gitignore": "ignored_dir\n",
1843            "ignored_dir": {
1844                "file1.txt": "content1",
1845                "file2.txt": "content2",
1846            },
1847        }),
1848    )
1849    .await;
1850
1851    let tree = Worktree::local(
1852        Path::new("/root"),
1853        true,
1854        fs.clone(),
1855        Default::default(),
1856        true,
1857        WorktreeId::from_proto(0),
1858        &mut cx.to_async(),
1859    )
1860    .await
1861    .unwrap();
1862
1863    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1864        .await;
1865
1866    // Load a file to expand the ignored directory
1867    tree.update(cx, |tree, cx| {
1868        tree.load_file(rel_path("ignored_dir/file1.txt"), cx)
1869    })
1870    .await
1871    .unwrap();
1872
1873    tree.read_with(cx, |tree, _| {
1874        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
1875        assert_eq!(ignored_dir.kind, EntryKind::Dir);
1876        assert!(
1877            tree.entry_for_path(rel_path("ignored_dir/file1.txt"))
1878                .is_some()
1879        );
1880        assert!(
1881            tree.entry_for_path(rel_path("ignored_dir/file2.txt"))
1882                .is_some()
1883        );
1884    });
1885
1886    fs.emit_fs_event("/root/ignored_dir", Some(fs::PathEventKind::Changed));
1887    tree.flush_fs_events(cx).await;
1888
1889    tree.read_with(cx, |tree, _| {
1890        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
1891        assert_eq!(
1892            ignored_dir.kind,
1893            EntryKind::Dir,
1894            "ignored_dir should still be loaded (Dir), not UnloadedDir"
1895        );
1896        assert!(
1897            tree.entry_for_path(rel_path("ignored_dir/file1.txt"))
1898                .is_some(),
1899            "file1.txt should still be visible after directory fs event"
1900        );
1901        assert!(
1902            tree.entry_for_path(rel_path("ignored_dir/file2.txt"))
1903                .is_some(),
1904            "file2.txt should still be visible after directory fs event"
1905        );
1906    });
1907}
1908
1909#[gpui::test(iterations = 100)]
1910async fn test_random_worktree_operations_during_initial_scan(
1911    cx: &mut TestAppContext,
1912    mut rng: StdRng,
1913) {
1914    init_test(cx);
1915    let operations = env::var("OPERATIONS")
1916        .map(|o| o.parse().unwrap())
1917        .unwrap_or(5);
1918    let initial_entries = env::var("INITIAL_ENTRIES")
1919        .map(|o| o.parse().unwrap())
1920        .unwrap_or(20);
1921
1922    let root_dir = Path::new(path!("/test"));
1923    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1924    fs.as_fake().insert_tree(root_dir, json!({})).await;
1925    for _ in 0..initial_entries {
1926        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1927    }
1928    log::info!("generated initial tree");
1929
1930    let worktree = Worktree::local(
1931        root_dir,
1932        true,
1933        fs.clone(),
1934        Default::default(),
1935        true,
1936        WorktreeId::from_proto(0),
1937        &mut cx.to_async(),
1938    )
1939    .await
1940    .unwrap();
1941
1942    let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1943    let updates = Arc::new(Mutex::new(Vec::new()));
1944    worktree.update(cx, |tree, cx| {
1945        check_worktree_change_events(tree, cx);
1946
1947        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1948            let updates = updates.clone();
1949            move |update| {
1950                updates.lock().push(update);
1951                async { true }
1952            }
1953        });
1954    });
1955
1956    for _ in 0..operations {
1957        worktree
1958            .update(cx, |worktree, cx| {
1959                randomly_mutate_worktree(worktree, &mut rng, cx)
1960            })
1961            .await
1962            .log_err();
1963        worktree.read_with(cx, |tree, _| {
1964            tree.as_local().unwrap().snapshot().check_invariants(true)
1965        });
1966
1967        if rng.random_bool(0.6) {
1968            snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1969        }
1970    }
1971
1972    worktree
1973        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1974        .await;
1975
1976    cx.executor().run_until_parked();
1977
1978    let final_snapshot = worktree.read_with(cx, |tree, _| {
1979        let tree = tree.as_local().unwrap();
1980        let snapshot = tree.snapshot();
1981        snapshot.check_invariants(true);
1982        snapshot
1983    });
1984
1985    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1986
1987    for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1988        let mut updated_snapshot = snapshot.clone();
1989        for update in updates.lock().iter() {
1990            if update.scan_id >= updated_snapshot.scan_id() as u64 {
1991                updated_snapshot
1992                    .apply_remote_update(update.clone(), &settings.file_scan_inclusions);
1993            }
1994        }
1995
1996        assert_eq!(
1997            updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1998            final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1999            "wrong updates after snapshot {i}: {updates:#?}",
2000        );
2001    }
2002}
2003
2004#[gpui::test(iterations = 100)]
2005async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
2006    init_test(cx);
2007    let operations = env::var("OPERATIONS")
2008        .map(|o| o.parse().unwrap())
2009        .unwrap_or(40);
2010    let initial_entries = env::var("INITIAL_ENTRIES")
2011        .map(|o| o.parse().unwrap())
2012        .unwrap_or(20);
2013
2014    let root_dir = Path::new(path!("/test"));
2015    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
2016    fs.as_fake().insert_tree(root_dir, json!({})).await;
2017    for _ in 0..initial_entries {
2018        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
2019    }
2020    log::info!("generated initial tree");
2021
2022    let worktree = Worktree::local(
2023        root_dir,
2024        true,
2025        fs.clone(),
2026        Default::default(),
2027        true,
2028        WorktreeId::from_proto(0),
2029        &mut cx.to_async(),
2030    )
2031    .await
2032    .unwrap();
2033
2034    let updates = Arc::new(Mutex::new(Vec::new()));
2035    worktree.update(cx, |tree, cx| {
2036        check_worktree_change_events(tree, cx);
2037
2038        tree.as_local_mut().unwrap().observe_updates(0, cx, {
2039            let updates = updates.clone();
2040            move |update| {
2041                updates.lock().push(update);
2042                async { true }
2043            }
2044        });
2045    });
2046
2047    worktree
2048        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
2049        .await;
2050
2051    fs.as_fake().pause_events();
2052    let mut snapshots = Vec::new();
2053    let mut mutations_len = operations;
2054    while mutations_len > 1 {
2055        if rng.random_bool(0.2) {
2056            worktree
2057                .update(cx, |worktree, cx| {
2058                    randomly_mutate_worktree(worktree, &mut rng, cx)
2059                })
2060                .await
2061                .log_err();
2062        } else {
2063            randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
2064        }
2065
2066        let buffered_event_count = fs.as_fake().buffered_event_count();
2067        if buffered_event_count > 0 && rng.random_bool(0.3) {
2068            let len = rng.random_range(0..=buffered_event_count);
2069            log::info!("flushing {} events", len);
2070            fs.as_fake().flush_events(len);
2071        } else {
2072            randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
2073            mutations_len -= 1;
2074        }
2075
2076        cx.executor().run_until_parked();
2077        if rng.random_bool(0.2) {
2078            log::info!("storing snapshot {}", snapshots.len());
2079            let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
2080            snapshots.push(snapshot);
2081        }
2082    }
2083
2084    log::info!("quiescing");
2085    fs.as_fake().flush_events(usize::MAX);
2086    cx.executor().run_until_parked();
2087
2088    let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
2089    snapshot.check_invariants(true);
2090    let expanded_paths = snapshot
2091        .expanded_entries()
2092        .map(|e| e.path.clone())
2093        .collect::<Vec<_>>();
2094
2095    {
2096        let new_worktree = Worktree::local(
2097            root_dir,
2098            true,
2099            fs.clone(),
2100            Default::default(),
2101            true,
2102            WorktreeId::from_proto(0),
2103            &mut cx.to_async(),
2104        )
2105        .await
2106        .unwrap();
2107        new_worktree
2108            .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
2109            .await;
2110        new_worktree
2111            .update(cx, |tree, _| {
2112                tree.as_local_mut()
2113                    .unwrap()
2114                    .refresh_entries_for_paths(expanded_paths)
2115            })
2116            .recv()
2117            .await;
2118        let new_snapshot =
2119            new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
2120        assert_eq!(
2121            snapshot.entries_without_ids(true),
2122            new_snapshot.entries_without_ids(true)
2123        );
2124    }
2125
2126    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
2127
2128    for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
2129        for update in updates.lock().iter() {
2130            if update.scan_id >= prev_snapshot.scan_id() as u64 {
2131                prev_snapshot.apply_remote_update(update.clone(), &settings.file_scan_inclusions);
2132            }
2133        }
2134
2135        assert_eq!(
2136            prev_snapshot
2137                .entries(true, 0)
2138                .map(ignore_pending_dir)
2139                .collect::<Vec<_>>(),
2140            snapshot
2141                .entries(true, 0)
2142                .map(ignore_pending_dir)
2143                .collect::<Vec<_>>(),
2144            "wrong updates after snapshot {i}: {updates:#?}",
2145        );
2146    }
2147
2148    fn ignore_pending_dir(entry: &Entry) -> Entry {
2149        let mut entry = entry.clone();
2150        if entry.kind.is_dir() {
2151            entry.kind = EntryKind::Dir
2152        }
2153        entry
2154    }
2155}
2156
2157// The worktree's `UpdatedEntries` event can be used to follow along with
2158// all changes to the worktree's snapshot.
2159fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
2160    let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
2161    cx.subscribe(&cx.entity(), move |tree, _, event, _| {
2162        if let Event::UpdatedEntries(changes) = event {
2163            for (path, _, change_type) in changes.iter() {
2164                let entry = tree.entry_for_path(path).cloned();
2165                let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
2166                    Ok(ix) | Err(ix) => ix,
2167                };
2168                match change_type {
2169                    PathChange::Added => entries.insert(ix, entry.unwrap()),
2170                    PathChange::Removed => drop(entries.remove(ix)),
2171                    PathChange::Updated => {
2172                        let entry = entry.unwrap();
2173                        let existing_entry = entries.get_mut(ix).unwrap();
2174                        assert_eq!(existing_entry.path, entry.path);
2175                        *existing_entry = entry;
2176                    }
2177                    PathChange::AddedOrUpdated | PathChange::Loaded => {
2178                        let entry = entry.unwrap();
2179                        if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
2180                            *entries.get_mut(ix).unwrap() = entry;
2181                        } else {
2182                            entries.insert(ix, entry);
2183                        }
2184                    }
2185                }
2186            }
2187
2188            let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
2189            assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
2190        }
2191    })
2192    .detach();
2193}
2194
2195fn randomly_mutate_worktree(
2196    worktree: &mut Worktree,
2197    rng: &mut impl Rng,
2198    cx: &mut Context<Worktree>,
2199) -> Task<Result<()>> {
2200    log::info!("mutating worktree");
2201    let worktree = worktree.as_local_mut().unwrap();
2202    let snapshot = worktree.snapshot();
2203    let entry = snapshot.entries(false, 0).choose(rng).unwrap();
2204
2205    match rng.random_range(0_u32..100) {
2206        0..=33 if entry.path.as_ref() != RelPath::empty() => {
2207            log::info!("deleting entry {:?} ({})", entry.path, entry.id.to_usize());
2208            worktree.delete_entry(entry.id, false, cx).unwrap()
2209        }
2210        _ => {
2211            if entry.is_dir() {
2212                let child_path = entry.path.join(rel_path(&random_filename(rng)));
2213                let is_dir = rng.random_bool(0.3);
2214                log::info!(
2215                    "creating {} at {:?}",
2216                    if is_dir { "dir" } else { "file" },
2217                    child_path,
2218                );
2219                let task = worktree.create_entry(child_path, is_dir, None, cx);
2220                cx.background_spawn(async move {
2221                    task.await?;
2222                    Ok(())
2223                })
2224            } else {
2225                log::info!(
2226                    "overwriting file {:?} ({})",
2227                    &entry.path,
2228                    entry.id.to_usize()
2229                );
2230                let task = worktree.write_file(
2231                    entry.path.clone(),
2232                    "".into(),
2233                    Default::default(),
2234                    encoding_rs::UTF_8,
2235                    false,
2236                    cx,
2237                );
2238                cx.background_spawn(async move {
2239                    task.await?;
2240                    Ok(())
2241                })
2242            }
2243        }
2244    }
2245}
2246
2247async fn randomly_mutate_fs(
2248    fs: &Arc<dyn Fs>,
2249    root_path: &Path,
2250    insertion_probability: f64,
2251    rng: &mut impl Rng,
2252) {
2253    log::info!("mutating fs");
2254    let mut files = Vec::new();
2255    let mut dirs = Vec::new();
2256    for path in fs.as_fake().paths(false) {
2257        if path.starts_with(root_path) {
2258            if fs.is_file(&path).await {
2259                files.push(path);
2260            } else {
2261                dirs.push(path);
2262            }
2263        }
2264    }
2265
2266    if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) {
2267        let path = dirs.choose(rng).unwrap();
2268        let new_path = path.join(random_filename(rng));
2269
2270        if rng.random() {
2271            log::info!(
2272                "creating dir {:?}",
2273                new_path.strip_prefix(root_path).unwrap()
2274            );
2275            fs.create_dir(&new_path).await.unwrap();
2276        } else {
2277            log::info!(
2278                "creating file {:?}",
2279                new_path.strip_prefix(root_path).unwrap()
2280            );
2281            fs.create_file(&new_path, Default::default()).await.unwrap();
2282        }
2283    } else if rng.random_bool(0.05) {
2284        let ignore_dir_path = dirs.choose(rng).unwrap();
2285        let ignore_path = ignore_dir_path.join(GITIGNORE);
2286
2287        let subdirs = dirs
2288            .iter()
2289            .filter(|d| d.starts_with(ignore_dir_path))
2290            .cloned()
2291            .collect::<Vec<_>>();
2292        let subfiles = files
2293            .iter()
2294            .filter(|d| d.starts_with(ignore_dir_path))
2295            .cloned()
2296            .collect::<Vec<_>>();
2297        let files_to_ignore = {
2298            let len = rng.random_range(0..=subfiles.len());
2299            subfiles.choose_multiple(rng, len)
2300        };
2301        let dirs_to_ignore = {
2302            let len = rng.random_range(0..subdirs.len());
2303            subdirs.choose_multiple(rng, len)
2304        };
2305
2306        let mut ignore_contents = String::new();
2307        for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
2308            writeln!(
2309                ignore_contents,
2310                "{}",
2311                path_to_ignore
2312                    .strip_prefix(ignore_dir_path)
2313                    .unwrap()
2314                    .to_str()
2315                    .unwrap()
2316            )
2317            .unwrap();
2318        }
2319        log::info!(
2320            "creating gitignore {:?} with contents:\n{}",
2321            ignore_path.strip_prefix(root_path).unwrap(),
2322            ignore_contents
2323        );
2324        fs.save(
2325            &ignore_path,
2326            &ignore_contents.as_str().into(),
2327            Default::default(),
2328        )
2329        .await
2330        .unwrap();
2331    } else {
2332        let old_path = {
2333            let file_path = files.choose(rng);
2334            let dir_path = dirs[1..].choose(rng);
2335            file_path.into_iter().chain(dir_path).choose(rng).unwrap()
2336        };
2337
2338        let is_rename = rng.random();
2339        if is_rename {
2340            let new_path_parent = dirs
2341                .iter()
2342                .filter(|d| !d.starts_with(old_path))
2343                .choose(rng)
2344                .unwrap();
2345
2346            let overwrite_existing_dir =
2347                !old_path.starts_with(new_path_parent) && rng.random_bool(0.3);
2348            let new_path = if overwrite_existing_dir {
2349                fs.remove_dir(
2350                    new_path_parent,
2351                    RemoveOptions {
2352                        recursive: true,
2353                        ignore_if_not_exists: true,
2354                    },
2355                )
2356                .await
2357                .unwrap();
2358                new_path_parent.to_path_buf()
2359            } else {
2360                new_path_parent.join(random_filename(rng))
2361            };
2362
2363            log::info!(
2364                "renaming {:?} to {}{:?}",
2365                old_path.strip_prefix(root_path).unwrap(),
2366                if overwrite_existing_dir {
2367                    "overwrite "
2368                } else {
2369                    ""
2370                },
2371                new_path.strip_prefix(root_path).unwrap()
2372            );
2373            fs.rename(
2374                old_path,
2375                &new_path,
2376                fs::RenameOptions {
2377                    overwrite: true,
2378                    ignore_if_exists: true,
2379                    create_parents: false,
2380                },
2381            )
2382            .await
2383            .unwrap();
2384        } else if fs.is_file(old_path).await {
2385            log::info!(
2386                "deleting file {:?}",
2387                old_path.strip_prefix(root_path).unwrap()
2388            );
2389            fs.remove_file(old_path, Default::default()).await.unwrap();
2390        } else {
2391            log::info!(
2392                "deleting dir {:?}",
2393                old_path.strip_prefix(root_path).unwrap()
2394            );
2395            fs.remove_dir(
2396                old_path,
2397                RemoveOptions {
2398                    recursive: true,
2399                    ignore_if_not_exists: true,
2400                },
2401            )
2402            .await
2403            .unwrap();
2404        }
2405    }
2406}
2407
2408fn random_filename(rng: &mut impl Rng) -> String {
2409    (0..6)
2410        .map(|_| rng.sample(rand::distr::Alphanumeric))
2411        .map(char::from)
2412        .collect()
2413}
2414
2415#[gpui::test]
2416async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
2417    init_test(cx);
2418    let fs = FakeFs::new(cx.background_executor.clone());
2419    fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
2420        .await;
2421    let tree = Worktree::local(
2422        Path::new("/.env"),
2423        true,
2424        fs.clone(),
2425        Default::default(),
2426        true,
2427        WorktreeId::from_proto(0),
2428        &mut cx.to_async(),
2429    )
2430    .await
2431    .unwrap();
2432    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2433        .await;
2434    tree.read_with(cx, |tree, _| {
2435        let entry = tree.entry_for_path(rel_path("")).unwrap();
2436        assert!(entry.is_private);
2437    });
2438}
2439
2440#[gpui::test]
2441async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2442    init_test(cx);
2443
2444    let fs = FakeFs::new(executor);
2445    fs.insert_tree(
2446        path!("/root"),
2447        json!({
2448            ".git": {},
2449            "subproject": {
2450                "a.txt": "A"
2451            }
2452        }),
2453    )
2454    .await;
2455    let worktree = Worktree::local(
2456        path!("/root/subproject").as_ref(),
2457        true,
2458        fs.clone(),
2459        Arc::default(),
2460        true,
2461        WorktreeId::from_proto(0),
2462        &mut cx.to_async(),
2463    )
2464    .await
2465    .unwrap();
2466    worktree
2467        .update(cx, |worktree, _| {
2468            worktree.as_local().unwrap().scan_complete()
2469        })
2470        .await;
2471    cx.run_until_parked();
2472    let repos = worktree.update(cx, |worktree, _| {
2473        worktree.as_local().unwrap().repositories()
2474    });
2475    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2476
2477    fs.touch_path(path!("/root/subproject")).await;
2478    worktree
2479        .update(cx, |worktree, _| {
2480            worktree.as_local().unwrap().scan_complete()
2481        })
2482        .await;
2483    cx.run_until_parked();
2484
2485    let repos = worktree.update(cx, |worktree, _| {
2486        worktree.as_local().unwrap().repositories()
2487    });
2488    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2489}
2490
2491#[gpui::test]
2492async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2493    init_test(cx);
2494
2495    let home = paths::home_dir();
2496    let fs = FakeFs::new(executor);
2497    fs.insert_tree(
2498        home,
2499        json!({
2500            ".config": {
2501                "git": {
2502                    "ignore": "foo\n/bar\nbaz\n"
2503                }
2504            },
2505            "project": {
2506                ".git": {},
2507                ".gitignore": "!baz",
2508                "foo": "",
2509                "bar": "",
2510                "sub": {
2511                    "bar": "",
2512                },
2513                "subrepo": {
2514                    ".git": {},
2515                    "bar": ""
2516                },
2517                "baz": ""
2518            }
2519        }),
2520    )
2521    .await;
2522    let worktree = Worktree::local(
2523        home.join("project"),
2524        true,
2525        fs.clone(),
2526        Arc::default(),
2527        true,
2528        WorktreeId::from_proto(0),
2529        &mut cx.to_async(),
2530    )
2531    .await
2532    .unwrap();
2533    worktree
2534        .update(cx, |worktree, _| {
2535            worktree.as_local().unwrap().scan_complete()
2536        })
2537        .await;
2538    cx.run_until_parked();
2539
2540    // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved
2541    // relative to the nearest containing repository
2542    worktree.update(cx, |worktree, _cx| {
2543        check_worktree_entries(
2544            worktree,
2545            &[],
2546            &["foo", "bar", "subrepo/bar"],
2547            &["sub/bar", "baz"],
2548            &[],
2549        );
2550    });
2551
2552    // Ignore statuses are updated when excludesFile changes
2553    fs.write(
2554        &home.join(".config").join("git").join("ignore"),
2555        "/bar\nbaz\n".as_bytes(),
2556    )
2557    .await
2558    .unwrap();
2559    worktree
2560        .update(cx, |worktree, _| {
2561            worktree.as_local().unwrap().scan_complete()
2562        })
2563        .await;
2564    cx.run_until_parked();
2565
2566    worktree.update(cx, |worktree, _cx| {
2567        check_worktree_entries(
2568            worktree,
2569            &[],
2570            &["bar", "subrepo/bar"],
2571            &["foo", "sub/bar", "baz"],
2572            &[],
2573        );
2574    });
2575
2576    // Statuses are updated when .git added/removed
2577    fs.remove_dir(
2578        &home.join("project").join("subrepo").join(".git"),
2579        RemoveOptions {
2580            recursive: true,
2581            ..Default::default()
2582        },
2583    )
2584    .await
2585    .unwrap();
2586    worktree
2587        .update(cx, |worktree, _| {
2588            worktree.as_local().unwrap().scan_complete()
2589        })
2590        .await;
2591    cx.run_until_parked();
2592
2593    worktree.update(cx, |worktree, _cx| {
2594        check_worktree_entries(
2595            worktree,
2596            &[],
2597            &["bar"],
2598            &["foo", "sub/bar", "baz", "subrepo/bar"],
2599            &[],
2600        );
2601    });
2602}
2603
2604#[gpui::test]
2605async fn test_repo_exclude(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2606    init_test(cx);
2607
2608    let fs = FakeFs::new(executor);
2609    let project_dir = Path::new(path!("/project"));
2610    fs.insert_tree(
2611        project_dir,
2612        json!({
2613            ".git": {
2614                "info": {
2615                    "exclude": ".env.*"
2616                }
2617            },
2618            ".env.example": "secret=xxxx",
2619            ".env.local": "secret=1234",
2620            ".gitignore": "!.env.example",
2621            "README.md": "# Repo Exclude",
2622            "src": {
2623                "main.rs": "fn main() {}",
2624            },
2625        }),
2626    )
2627    .await;
2628
2629    let worktree = Worktree::local(
2630        project_dir,
2631        true,
2632        fs.clone(),
2633        Default::default(),
2634        true,
2635        WorktreeId::from_proto(0),
2636        &mut cx.to_async(),
2637    )
2638    .await
2639    .unwrap();
2640    worktree
2641        .update(cx, |worktree, _| {
2642            worktree.as_local().unwrap().scan_complete()
2643        })
2644        .await;
2645    cx.run_until_parked();
2646
2647    // .gitignore overrides .git/info/exclude
2648    worktree.update(cx, |worktree, _cx| {
2649        let expected_excluded_paths = [];
2650        let expected_ignored_paths = [".env.local"];
2651        let expected_tracked_paths = [".env.example", "README.md", "src/main.rs"];
2652        let expected_included_paths = [];
2653
2654        check_worktree_entries(
2655            worktree,
2656            &expected_excluded_paths,
2657            &expected_ignored_paths,
2658            &expected_tracked_paths,
2659            &expected_included_paths,
2660        );
2661    });
2662
2663    // Ignore statuses are updated when .git/info/exclude file changes
2664    fs.write(
2665        &project_dir.join(DOT_GIT).join(REPO_EXCLUDE),
2666        ".env.example".as_bytes(),
2667    )
2668    .await
2669    .unwrap();
2670    worktree
2671        .update(cx, |worktree, _| {
2672            worktree.as_local().unwrap().scan_complete()
2673        })
2674        .await;
2675    cx.run_until_parked();
2676
2677    worktree.update(cx, |worktree, _cx| {
2678        let expected_excluded_paths = [];
2679        let expected_ignored_paths = [];
2680        let expected_tracked_paths = [".env.example", ".env.local", "README.md", "src/main.rs"];
2681        let expected_included_paths = [];
2682
2683        check_worktree_entries(
2684            worktree,
2685            &expected_excluded_paths,
2686            &expected_ignored_paths,
2687            &expected_tracked_paths,
2688            &expected_included_paths,
2689        );
2690    });
2691}
2692
2693#[track_caller]
2694fn check_worktree_entries(
2695    tree: &Worktree,
2696    expected_excluded_paths: &[&str],
2697    expected_ignored_paths: &[&str],
2698    expected_tracked_paths: &[&str],
2699    expected_included_paths: &[&str],
2700) {
2701    for path in expected_excluded_paths {
2702        let entry = tree.entry_for_path(rel_path(path));
2703        assert!(
2704            entry.is_none(),
2705            "expected path '{path}' to be excluded, but got entry: {entry:?}",
2706        );
2707    }
2708    for path in expected_ignored_paths {
2709        let entry = tree
2710            .entry_for_path(rel_path(path))
2711            .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2712        assert!(
2713            entry.is_ignored,
2714            "expected path '{path}' to be ignored, but got entry: {entry:?}",
2715        );
2716    }
2717    for path in expected_tracked_paths {
2718        let entry = tree
2719            .entry_for_path(rel_path(path))
2720            .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2721        assert!(
2722            !entry.is_ignored || entry.is_always_included,
2723            "expected path '{path}' to be tracked, but got entry: {entry:?}",
2724        );
2725    }
2726    for path in expected_included_paths {
2727        let entry = tree
2728            .entry_for_path(rel_path(path))
2729            .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
2730        assert!(
2731            entry.is_always_included,
2732            "expected path '{path}' to always be included, but got entry: {entry:?}",
2733        );
2734    }
2735}
2736
2737fn init_test(cx: &mut gpui::TestAppContext) {
2738    zlog::init_test();
2739
2740    cx.update(|cx| {
2741        let settings_store = SettingsStore::test(cx);
2742        cx.set_global(settings_store);
2743    });
2744}
2745
2746#[gpui::test]
2747async fn test_load_file_encoding(cx: &mut TestAppContext) {
2748    init_test(cx);
2749
2750    struct TestCase {
2751        name: &'static str,
2752        bytes: Vec<u8>,
2753        expected_text: &'static str,
2754    }
2755
2756    // --- Success Cases ---
2757    let success_cases = vec![
2758        TestCase {
2759            name: "utf8.txt",
2760            bytes: "ใ“ใ‚“ใซใกใฏ".as_bytes().to_vec(),
2761            expected_text: "ใ“ใ‚“ใซใกใฏ",
2762        },
2763        TestCase {
2764            name: "sjis.txt",
2765            bytes: vec![0x82, 0xb1, 0x82, 0xf1, 0x82, 0xc9, 0x82, 0xbf, 0x82, 0xcd],
2766            expected_text: "ใ“ใ‚“ใซใกใฏ",
2767        },
2768        TestCase {
2769            name: "eucjp.txt",
2770            bytes: vec![0xa4, 0xb3, 0xa4, 0xf3, 0xa4, 0xcb, 0xa4, 0xc1, 0xa4, 0xcf],
2771            expected_text: "ใ“ใ‚“ใซใกใฏ",
2772        },
2773        TestCase {
2774            name: "iso2022jp.txt",
2775            bytes: vec![
2776                0x1b, 0x24, 0x42, 0x24, 0x33, 0x24, 0x73, 0x24, 0x4b, 0x24, 0x41, 0x24, 0x4f, 0x1b,
2777                0x28, 0x42,
2778            ],
2779            expected_text: "ใ“ใ‚“ใซใกใฏ",
2780        },
2781        TestCase {
2782            name: "win1252.txt",
2783            bytes: vec![0x43, 0x61, 0x66, 0xe9],
2784            expected_text: "Cafรฉ",
2785        },
2786        TestCase {
2787            name: "gbk.txt",
2788            bytes: vec![
2789                0xbd, 0xf1, 0xcc, 0xec, 0xcc, 0xec, 0xc6, 0xf8, 0xb2, 0xbb, 0xb4, 0xed,
2790            ],
2791            expected_text: "ไปŠๅคฉๅคฉๆฐ”ไธ้”™",
2792        },
2793        // UTF-16LE with BOM
2794        TestCase {
2795            name: "utf16le_bom.txt",
2796            bytes: vec![
2797                0xFF, 0xFE, // BOM
2798                0x53, 0x30, 0x93, 0x30, 0x6B, 0x30, 0x61, 0x30, 0x6F, 0x30,
2799            ],
2800            expected_text: "ใ“ใ‚“ใซใกใฏ",
2801        },
2802        // UTF-16BE with BOM
2803        TestCase {
2804            name: "utf16be_bom.txt",
2805            bytes: vec![
2806                0xFE, 0xFF, // BOM
2807                0x30, 0x53, 0x30, 0x93, 0x30, 0x6B, 0x30, 0x61, 0x30, 0x6F,
2808            ],
2809            expected_text: "ใ“ใ‚“ใซใกใฏ",
2810        },
2811        // UTF-16LE without BOM (ASCII only)
2812        // This relies on the "null byte heuristic" we implemented.
2813        // "ABC" -> 41 00 42 00 43 00
2814        TestCase {
2815            name: "utf16le_ascii_no_bom.txt",
2816            bytes: vec![0x41, 0x00, 0x42, 0x00, 0x43, 0x00],
2817            expected_text: "ABC",
2818        },
2819    ];
2820
2821    // --- Failure Cases ---
2822    let failure_cases = vec![
2823        // Binary File (Should be detected by heuristic and return Error)
2824        // Contains random bytes and mixed nulls that don't match UTF-16 patterns
2825        TestCase {
2826            name: "binary.bin",
2827            bytes: vec![0x00, 0xFF, 0x12, 0x00, 0x99, 0x88, 0x77, 0x66, 0x00],
2828            expected_text: "", // Not used
2829        },
2830    ];
2831
2832    let root_path = if cfg!(windows) {
2833        Path::new("C:\\root")
2834    } else {
2835        Path::new("/root")
2836    };
2837
2838    let fs = FakeFs::new(cx.background_executor.clone());
2839    fs.create_dir(root_path).await.unwrap();
2840
2841    for case in success_cases.iter().chain(failure_cases.iter()) {
2842        let path = root_path.join(case.name);
2843        fs.write(&path, &case.bytes).await.unwrap();
2844    }
2845
2846    let tree = Worktree::local(
2847        root_path,
2848        true,
2849        fs,
2850        Default::default(),
2851        true,
2852        WorktreeId::from_proto(0),
2853        &mut cx.to_async(),
2854    )
2855    .await
2856    .unwrap();
2857
2858    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2859        .await;
2860
2861    let rel_path = |name: &str| {
2862        RelPath::new(&Path::new(name), PathStyle::local())
2863            .unwrap()
2864            .into_arc()
2865    };
2866
2867    // Run Success Tests
2868    for case in success_cases {
2869        let loaded = tree
2870            .update(cx, |tree, cx| tree.load_file(&rel_path(case.name), cx))
2871            .await;
2872        if let Err(e) = &loaded {
2873            panic!("Failed to load success case '{}': {:?}", case.name, e);
2874        }
2875        let loaded = loaded.unwrap();
2876        assert_eq!(
2877            loaded.text, case.expected_text,
2878            "Encoding mismatch for file: {}",
2879            case.name
2880        );
2881    }
2882
2883    // Run Failure Tests
2884    for case in failure_cases {
2885        let loaded = tree
2886            .update(cx, |tree, cx| tree.load_file(&rel_path(case.name), cx))
2887            .await;
2888        assert!(
2889            loaded.is_err(),
2890            "Failure case '{}' unexpectedly succeeded! It should have been detected as binary.",
2891            case.name
2892        );
2893        let err_msg = loaded.unwrap_err().to_string();
2894        println!("Got expected error for {}: {}", case.name, err_msg);
2895    }
2896}
2897
2898#[gpui::test]
2899async fn test_write_file_encoding(cx: &mut gpui::TestAppContext) {
2900    init_test(cx);
2901    let fs = FakeFs::new(cx.executor());
2902
2903    let root_path = if cfg!(windows) {
2904        Path::new("C:\\root")
2905    } else {
2906        Path::new("/root")
2907    };
2908    fs.create_dir(root_path).await.unwrap();
2909
2910    let worktree = Worktree::local(
2911        root_path,
2912        true,
2913        fs.clone(),
2914        Default::default(),
2915        true,
2916        WorktreeId::from_proto(0),
2917        &mut cx.to_async(),
2918    )
2919    .await
2920    .unwrap();
2921
2922    // Define test case structure
2923    struct TestCase {
2924        name: &'static str,
2925        text: &'static str,
2926        encoding: &'static encoding_rs::Encoding,
2927        has_bom: bool,
2928        expected_bytes: Vec<u8>,
2929    }
2930
2931    let cases = vec![
2932        // Shift_JIS with Japanese
2933        TestCase {
2934            name: "Shift_JIS with Japanese",
2935            text: "ใ“ใ‚“ใซใกใฏ",
2936            encoding: encoding_rs::SHIFT_JIS,
2937            has_bom: false,
2938            expected_bytes: vec![0x82, 0xb1, 0x82, 0xf1, 0x82, 0xc9, 0x82, 0xbf, 0x82, 0xcd],
2939        },
2940        // UTF-8 No BOM
2941        TestCase {
2942            name: "UTF-8 No BOM",
2943            text: "AB",
2944            encoding: encoding_rs::UTF_8,
2945            has_bom: false,
2946            expected_bytes: vec![0x41, 0x42],
2947        },
2948        // UTF-8 with BOM
2949        TestCase {
2950            name: "UTF-8 with BOM",
2951            text: "AB",
2952            encoding: encoding_rs::UTF_8,
2953            has_bom: true,
2954            expected_bytes: vec![0xEF, 0xBB, 0xBF, 0x41, 0x42],
2955        },
2956        // UTF-16LE No BOM with Japanese
2957        // NOTE: This passes thanks to the manual encoding fix implemented in `write_file`.
2958        TestCase {
2959            name: "UTF-16LE No BOM with Japanese",
2960            text: "ใ“ใ‚“ใซใกใฏ",
2961            encoding: encoding_rs::UTF_16LE,
2962            has_bom: false,
2963            expected_bytes: vec![0x53, 0x30, 0x93, 0x30, 0x6b, 0x30, 0x61, 0x30, 0x6f, 0x30],
2964        },
2965        // UTF-16LE with BOM
2966        TestCase {
2967            name: "UTF-16LE with BOM",
2968            text: "A",
2969            encoding: encoding_rs::UTF_16LE,
2970            has_bom: true,
2971            expected_bytes: vec![0xFF, 0xFE, 0x41, 0x00],
2972        },
2973        // UTF-16BE No BOM with Japanese
2974        // NOTE: This passes thanks to the manual encoding fix.
2975        TestCase {
2976            name: "UTF-16BE No BOM with Japanese",
2977            text: "ใ“ใ‚“ใซใกใฏ",
2978            encoding: encoding_rs::UTF_16BE,
2979            has_bom: false,
2980            expected_bytes: vec![0x30, 0x53, 0x30, 0x93, 0x30, 0x6b, 0x30, 0x61, 0x30, 0x6f],
2981        },
2982        // UTF-16BE with BOM
2983        TestCase {
2984            name: "UTF-16BE with BOM",
2985            text: "A",
2986            encoding: encoding_rs::UTF_16BE,
2987            has_bom: true,
2988            expected_bytes: vec![0xFE, 0xFF, 0x00, 0x41],
2989        },
2990    ];
2991
2992    for (i, case) in cases.into_iter().enumerate() {
2993        let file_name = format!("test_{}.txt", i);
2994        let path: Arc<Path> = Path::new(&file_name).into();
2995        let file_path = root_path.join(&file_name);
2996
2997        fs.insert_file(&file_path, "".into()).await;
2998
2999        let rel_path = RelPath::new(&path, PathStyle::local()).unwrap().into_arc();
3000        let text = text::Rope::from(case.text);
3001
3002        let task = worktree.update(cx, |wt, cx| {
3003            wt.write_file(
3004                rel_path,
3005                text,
3006                text::LineEnding::Unix,
3007                case.encoding,
3008                case.has_bom,
3009                cx,
3010            )
3011        });
3012
3013        if let Err(e) = task.await {
3014            panic!("Unexpected error in case '{}': {:?}", case.name, e);
3015        }
3016
3017        let bytes = fs.load_bytes(&file_path).await.unwrap();
3018
3019        assert_eq!(
3020            bytes, case.expected_bytes,
3021            "case '{}' mismatch. Expected {:?}, but got {:?}",
3022            case.name, case.expected_bytes, bytes
3023        );
3024    }
3025}
3026
3027#[gpui::test]
3028async fn test_refresh_entries_for_paths_creates_ancestors(cx: &mut TestAppContext) {
3029    init_test(cx);
3030    let fs = FakeFs::new(cx.background_executor.clone());
3031    fs.insert_tree(
3032        "/root",
3033        json!({
3034            "a": {
3035                "b": {
3036                    "c": {
3037                        "deep_file.txt": "content",
3038                        "sibling.txt": "content"
3039                    },
3040                    "d": {
3041                        "under_sibling_dir.txt": "content"
3042                    }
3043                }
3044            }
3045        }),
3046    )
3047    .await;
3048
3049    let tree = Worktree::local(
3050        Path::new("/root"),
3051        true,
3052        fs.clone(),
3053        Default::default(),
3054        false, // Disable scanning so the initial scan doesn't discover any entries
3055        WorktreeId::from_proto(0),
3056        &mut cx.to_async(),
3057    )
3058    .await
3059    .unwrap();
3060
3061    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3062        .await;
3063
3064    tree.read_with(cx, |tree, _| {
3065        assert_eq!(
3066            tree.entries(true, 0)
3067                .map(|e| e.path.as_ref())
3068                .collect::<Vec<_>>(),
3069            &[rel_path("")],
3070            "Only root entry should exist when scanning is disabled"
3071        );
3072
3073        assert!(tree.entry_for_path(rel_path("a")).is_none());
3074        assert!(tree.entry_for_path(rel_path("a/b")).is_none());
3075        assert!(tree.entry_for_path(rel_path("a/b/c")).is_none());
3076        assert!(
3077            tree.entry_for_path(rel_path("a/b/c/deep_file.txt"))
3078                .is_none()
3079        );
3080    });
3081
3082    tree.read_with(cx, |tree, _| {
3083        tree.as_local()
3084            .unwrap()
3085            .refresh_entries_for_paths(vec![rel_path("a/b/c/deep_file.txt").into()])
3086    })
3087    .recv()
3088    .await;
3089
3090    tree.read_with(cx, |tree, _| {
3091        assert_eq!(
3092            tree.entries(true, 0)
3093                .map(|e| e.path.as_ref())
3094                .collect::<Vec<_>>(),
3095            &[
3096                rel_path(""),
3097                rel_path("a"),
3098                rel_path("a/b"),
3099                rel_path("a/b/c"),
3100                rel_path("a/b/c/deep_file.txt"),
3101                rel_path("a/b/c/sibling.txt"),
3102                rel_path("a/b/d"),
3103            ],
3104            "All ancestors should be created when refreshing a deeply nested path"
3105        );
3106    });
3107}