remote_editing_tests.rs

   1use crate::headless_project::HeadlessProject;
   2use client::{Client, UserStore};
   3use clock::FakeSystemClock;
   4use fs::{FakeFs, Fs};
   5use gpui::{Context, Model, TestAppContext};
   6use http_client::{BlockedHttpClient, FakeHttpClient};
   7use language::{
   8    language_settings::{language_settings, AllLanguageSettings},
   9    Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName,
  10    LineEnding,
  11};
  12use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind};
  13use node_runtime::NodeRuntime;
  14use project::{
  15    search::{SearchQuery, SearchResult},
  16    Project, ProjectPath,
  17};
  18use remote::SshRemoteClient;
  19use serde_json::json;
  20use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore};
  21use smol::stream::StreamExt;
  22use std::{
  23    path::{Path, PathBuf},
  24    sync::Arc,
  25};
  26
  27#[gpui::test]
  28async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
  29    let fs = FakeFs::new(server_cx.executor());
  30    fs.insert_tree(
  31        "/code",
  32        json!({
  33            "project1": {
  34                ".git": {},
  35                "README.md": "# project 1",
  36                "src": {
  37                    "lib.rs": "fn one() -> usize { 1 }"
  38                }
  39            },
  40            "project2": {
  41                "README.md": "# project 2",
  42            },
  43        }),
  44    )
  45    .await;
  46    fs.set_index_for_repo(
  47        Path::new("/code/project1/.git"),
  48        &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
  49    );
  50
  51    let (project, _headless) = init_test(&fs, cx, server_cx).await;
  52    let (worktree, _) = project
  53        .update(cx, |project, cx| {
  54            project.find_or_create_worktree("/code/project1", true, cx)
  55        })
  56        .await
  57        .unwrap();
  58
  59    // The client sees the worktree's contents.
  60    cx.executor().run_until_parked();
  61    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
  62    worktree.update(cx, |worktree, _cx| {
  63        assert_eq!(
  64            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
  65            vec![
  66                Path::new("README.md"),
  67                Path::new("src"),
  68                Path::new("src/lib.rs"),
  69            ]
  70        );
  71    });
  72
  73    // The user opens a buffer in the remote worktree. The buffer's
  74    // contents are loaded from the remote filesystem.
  75    let buffer = project
  76        .update(cx, |project, cx| {
  77            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
  78        })
  79        .await
  80        .unwrap();
  81
  82    buffer.update(cx, |buffer, cx| {
  83        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
  84        assert_eq!(
  85            buffer.diff_base().unwrap().to_string(),
  86            "fn one() -> usize { 0 }"
  87        );
  88        let ix = buffer.text().find('1').unwrap();
  89        buffer.edit([(ix..ix + 1, "100")], None, cx);
  90    });
  91
  92    // The user saves the buffer. The new contents are written to the
  93    // remote filesystem.
  94    project
  95        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
  96        .await
  97        .unwrap();
  98    assert_eq!(
  99        fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
 100        "fn one() -> usize { 100 }"
 101    );
 102
 103    // A new file is created in the remote filesystem. The user
 104    // sees the new file.
 105    fs.save(
 106        "/code/project1/src/main.rs".as_ref(),
 107        &"fn main() {}".into(),
 108        Default::default(),
 109    )
 110    .await
 111    .unwrap();
 112    cx.executor().run_until_parked();
 113    worktree.update(cx, |worktree, _cx| {
 114        assert_eq!(
 115            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
 116            vec![
 117                Path::new("README.md"),
 118                Path::new("src"),
 119                Path::new("src/lib.rs"),
 120                Path::new("src/main.rs"),
 121            ]
 122        );
 123    });
 124
 125    // A file that is currently open in a buffer is renamed.
 126    fs.rename(
 127        "/code/project1/src/lib.rs".as_ref(),
 128        "/code/project1/src/lib2.rs".as_ref(),
 129        Default::default(),
 130    )
 131    .await
 132    .unwrap();
 133    cx.executor().run_until_parked();
 134    buffer.update(cx, |buffer, _| {
 135        assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
 136    });
 137
 138    fs.set_index_for_repo(
 139        Path::new("/code/project1/.git"),
 140        &[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())],
 141    );
 142    cx.executor().run_until_parked();
 143    buffer.update(cx, |buffer, _| {
 144        assert_eq!(
 145            buffer.diff_base().unwrap().to_string(),
 146            "fn one() -> usize { 100 }"
 147        );
 148    });
 149}
 150
 151#[gpui::test]
 152async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 153    let fs = FakeFs::new(server_cx.executor());
 154    fs.insert_tree(
 155        "/code",
 156        json!({
 157            "project1": {
 158                ".git": {},
 159                "README.md": "# project 1",
 160                "src": {
 161                    "lib.rs": "fn one() -> usize { 1 }"
 162                }
 163            },
 164        }),
 165    )
 166    .await;
 167
 168    let (project, headless) = init_test(&fs, cx, server_cx).await;
 169
 170    project
 171        .update(cx, |project, cx| {
 172            project.find_or_create_worktree("/code/project1", true, cx)
 173        })
 174        .await
 175        .unwrap();
 176
 177    cx.run_until_parked();
 178
 179    async fn do_search(project: &Model<Project>, mut cx: TestAppContext) -> Model<Buffer> {
 180        let mut receiver = project.update(&mut cx, |project, cx| {
 181            project.search(
 182                SearchQuery::text(
 183                    "project",
 184                    false,
 185                    true,
 186                    false,
 187                    Default::default(),
 188                    Default::default(),
 189                    None,
 190                )
 191                .unwrap(),
 192                cx,
 193            )
 194        });
 195
 196        let first_response = receiver.next().await.unwrap();
 197        let SearchResult::Buffer { buffer, .. } = first_response else {
 198            panic!("incorrect result");
 199        };
 200        buffer.update(&mut cx, |buffer, cx| {
 201            assert_eq!(
 202                buffer.file().unwrap().full_path(cx).to_string_lossy(),
 203                "project1/README.md"
 204            )
 205        });
 206
 207        assert!(receiver.next().await.is_none());
 208        buffer
 209    }
 210
 211    let buffer = do_search(&project, cx.clone()).await;
 212
 213    // test that the headless server is tracking which buffers we have open correctly.
 214    cx.run_until_parked();
 215    headless.update(server_cx, |headless, cx| {
 216        assert!(!headless.buffer_store.read(cx).shared_buffers().is_empty())
 217    });
 218    do_search(&project, cx.clone()).await;
 219
 220    cx.update(|_| {
 221        drop(buffer);
 222    });
 223    cx.run_until_parked();
 224    headless.update(server_cx, |headless, cx| {
 225        assert!(headless.buffer_store.read(cx).shared_buffers().is_empty())
 226    });
 227
 228    do_search(&project, cx.clone()).await;
 229}
 230
 231#[gpui::test]
 232async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 233    let fs = FakeFs::new(server_cx.executor());
 234    fs.insert_tree(
 235        "/code",
 236        json!({
 237            "project1": {
 238                ".git": {},
 239                "README.md": "# project 1",
 240                "src": {
 241                    "lib.rs": "fn one() -> usize { 1 }"
 242                }
 243            },
 244        }),
 245    )
 246    .await;
 247
 248    let (project, headless) = init_test(&fs, cx, server_cx).await;
 249
 250    cx.update_global(|settings_store: &mut SettingsStore, cx| {
 251        settings_store.set_user_settings(
 252            r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
 253            cx,
 254        )
 255    })
 256    .unwrap();
 257
 258    cx.run_until_parked();
 259
 260    server_cx.read(|cx| {
 261        assert_eq!(
 262            AllLanguageSettings::get_global(cx)
 263                .language(None, Some(&"Rust".into()), cx)
 264                .language_servers,
 265            ["..."] // local settings are ignored
 266        )
 267    });
 268
 269    server_cx
 270        .update_global(|settings_store: &mut SettingsStore, cx| {
 271            settings_store.set_server_settings(
 272                r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
 273                cx,
 274            )
 275        })
 276        .unwrap();
 277
 278    cx.run_until_parked();
 279
 280    server_cx.read(|cx| {
 281        assert_eq!(
 282            AllLanguageSettings::get_global(cx)
 283                .language(None, Some(&"Rust".into()), cx)
 284                .language_servers,
 285            ["from-server-settings".to_string()]
 286        )
 287    });
 288
 289    fs.insert_tree(
 290        "/code/project1/.zed",
 291        json!({
 292            "settings.json": r#"
 293                  {
 294                    "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
 295                    "lsp": {
 296                      "override-rust-analyzer": {
 297                        "binary": {
 298                          "path": "~/.cargo/bin/rust-analyzer"
 299                        }
 300                      }
 301                    }
 302                  }"#
 303        }),
 304    )
 305    .await;
 306
 307    let worktree_id = project
 308        .update(cx, |project, cx| {
 309            project.find_or_create_worktree("/code/project1", true, cx)
 310        })
 311        .await
 312        .unwrap()
 313        .0
 314        .read_with(cx, |worktree, _| worktree.id());
 315
 316    let buffer = project
 317        .update(cx, |project, cx| {
 318            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 319        })
 320        .await
 321        .unwrap();
 322    cx.run_until_parked();
 323
 324    server_cx.read(|cx| {
 325        let worktree_id = headless
 326            .read(cx)
 327            .worktree_store
 328            .read(cx)
 329            .worktrees()
 330            .next()
 331            .unwrap()
 332            .read(cx)
 333            .id();
 334        assert_eq!(
 335            AllLanguageSettings::get(
 336                Some(SettingsLocation {
 337                    worktree_id,
 338                    path: Path::new("src/lib.rs")
 339                }),
 340                cx
 341            )
 342            .language(None, Some(&"Rust".into()), cx)
 343            .language_servers,
 344            ["override-rust-analyzer".to_string()]
 345        )
 346    });
 347
 348    cx.read(|cx| {
 349        let file = buffer.read(cx).file();
 350        assert_eq!(
 351            language_settings(Some("Rust".into()), file, cx).language_servers,
 352            ["override-rust-analyzer".to_string()]
 353        )
 354    });
 355}
 356
 357#[gpui::test]
 358async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 359    let fs = FakeFs::new(server_cx.executor());
 360    fs.insert_tree(
 361        "/code",
 362        json!({
 363            "project1": {
 364                ".git": {},
 365                "README.md": "# project 1",
 366                "src": {
 367                    "lib.rs": "fn one() -> usize { 1 }"
 368                }
 369            },
 370        }),
 371    )
 372    .await;
 373
 374    let (project, headless) = init_test(&fs, cx, server_cx).await;
 375
 376    fs.insert_tree(
 377        "/code/project1/.zed",
 378        json!({
 379            "settings.json": r#"
 380          {
 381            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
 382            "lsp": {
 383              "rust-analyzer": {
 384                "binary": {
 385                  "path": "~/.cargo/bin/rust-analyzer"
 386                }
 387              }
 388            }
 389          }"#
 390        }),
 391    )
 392    .await;
 393
 394    cx.update_model(&project, |project, _| {
 395        project.languages().register_test_language(LanguageConfig {
 396            name: "Rust".into(),
 397            matcher: LanguageMatcher {
 398                path_suffixes: vec!["rs".into()],
 399                ..Default::default()
 400            },
 401            ..Default::default()
 402        });
 403        project.languages().register_fake_lsp_adapter(
 404            "Rust",
 405            FakeLspAdapter {
 406                name: "rust-analyzer",
 407                ..Default::default()
 408            },
 409        )
 410    });
 411
 412    let mut fake_lsp = server_cx.update(|cx| {
 413        headless.read(cx).languages.register_fake_language_server(
 414            LanguageServerName("rust-analyzer".into()),
 415            Default::default(),
 416            None,
 417        )
 418    });
 419
 420    cx.run_until_parked();
 421
 422    let worktree_id = project
 423        .update(cx, |project, cx| {
 424            project.find_or_create_worktree("/code/project1", true, cx)
 425        })
 426        .await
 427        .unwrap()
 428        .0
 429        .read_with(cx, |worktree, _| worktree.id());
 430
 431    // Wait for the settings to synchronize
 432    cx.run_until_parked();
 433
 434    let buffer = project
 435        .update(cx, |project, cx| {
 436            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 437        })
 438        .await
 439        .unwrap();
 440    cx.run_until_parked();
 441
 442    let fake_lsp = fake_lsp.next().await.unwrap();
 443
 444    cx.read(|cx| {
 445        let file = buffer.read(cx).file();
 446        assert_eq!(
 447            language_settings(Some("Rust".into()), file, cx).language_servers,
 448            ["rust-analyzer".to_string()]
 449        )
 450    });
 451
 452    let buffer_id = cx.read(|cx| {
 453        let buffer = buffer.read(cx);
 454        assert_eq!(buffer.language().unwrap().name(), "Rust".into());
 455        buffer.remote_id()
 456    });
 457
 458    server_cx.read(|cx| {
 459        let buffer = headless
 460            .read(cx)
 461            .buffer_store
 462            .read(cx)
 463            .get(buffer_id)
 464            .unwrap();
 465
 466        assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into());
 467    });
 468
 469    server_cx.read(|cx| {
 470        let lsp_store = headless.read(cx).lsp_store.read(cx);
 471        assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
 472    });
 473
 474    fake_lsp.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
 475        Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
 476            label: "boop".to_string(),
 477            ..Default::default()
 478        }])))
 479    });
 480
 481    let result = project
 482        .update(cx, |project, cx| {
 483            project.completions(
 484                &buffer,
 485                0,
 486                CompletionContext {
 487                    trigger_kind: CompletionTriggerKind::INVOKED,
 488                    trigger_character: None,
 489                },
 490                cx,
 491            )
 492        })
 493        .await
 494        .unwrap();
 495
 496    assert_eq!(
 497        result.into_iter().map(|c| c.label.text).collect::<Vec<_>>(),
 498        vec!["boop".to_string()]
 499    );
 500
 501    fake_lsp.handle_request::<lsp::request::Rename, _, _>(|_, _| async move {
 502        Ok(Some(lsp::WorkspaceEdit {
 503            changes: Some(
 504                [(
 505                    lsp::Url::from_file_path("/code/project1/src/lib.rs").unwrap(),
 506                    vec![lsp::TextEdit::new(
 507                        lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
 508                        "two".to_string(),
 509                    )],
 510                )]
 511                .into_iter()
 512                .collect(),
 513            ),
 514            ..Default::default()
 515        }))
 516    });
 517
 518    project
 519        .update(cx, |project, cx| {
 520            project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
 521        })
 522        .await
 523        .unwrap();
 524
 525    cx.run_until_parked();
 526    buffer.update(cx, |buffer, _| {
 527        assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
 528    })
 529}
 530
 531#[gpui::test]
 532async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 533    let fs = FakeFs::new(server_cx.executor());
 534    fs.insert_tree(
 535        "/code",
 536        json!({
 537            "project1": {
 538                ".git": {},
 539                "README.md": "# project 1",
 540                "src": {
 541                    "lib.rs": "fn one() -> usize { 1 }"
 542                }
 543            },
 544        }),
 545    )
 546    .await;
 547
 548    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 549    let (worktree, _) = project
 550        .update(cx, |project, cx| {
 551            project.find_or_create_worktree("/code/project1", true, cx)
 552        })
 553        .await
 554        .unwrap();
 555
 556    let worktree_id = cx.update(|cx| worktree.read(cx).id());
 557
 558    let buffer = project
 559        .update(cx, |project, cx| {
 560            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 561        })
 562        .await
 563        .unwrap();
 564
 565    fs.save(
 566        &PathBuf::from("/code/project1/src/lib.rs"),
 567        &("bangles".to_string().into()),
 568        LineEnding::Unix,
 569    )
 570    .await
 571    .unwrap();
 572
 573    cx.run_until_parked();
 574
 575    buffer.update(cx, |buffer, cx| {
 576        assert_eq!(buffer.text(), "bangles");
 577        buffer.edit([(0..0, "a")], None, cx);
 578    });
 579
 580    fs.save(
 581        &PathBuf::from("/code/project1/src/lib.rs"),
 582        &("bloop".to_string().into()),
 583        LineEnding::Unix,
 584    )
 585    .await
 586    .unwrap();
 587
 588    cx.run_until_parked();
 589    cx.update(|cx| {
 590        assert!(buffer.read(cx).has_conflict());
 591    });
 592
 593    project
 594        .update(cx, |project, cx| {
 595            project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
 596        })
 597        .await
 598        .unwrap();
 599    cx.run_until_parked();
 600
 601    cx.update(|cx| {
 602        assert!(!buffer.read(cx).has_conflict());
 603    });
 604}
 605
 606#[gpui::test]
 607async fn test_remote_resolve_path_in_buffer(
 608    cx: &mut TestAppContext,
 609    server_cx: &mut TestAppContext,
 610) {
 611    let fs = FakeFs::new(server_cx.executor());
 612    fs.insert_tree(
 613        "/code",
 614        json!({
 615            "project1": {
 616                ".git": {},
 617                "README.md": "# project 1",
 618                "src": {
 619                    "lib.rs": "fn one() -> usize { 1 }"
 620                }
 621            },
 622        }),
 623    )
 624    .await;
 625
 626    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 627    let (worktree, _) = project
 628        .update(cx, |project, cx| {
 629            project.find_or_create_worktree("/code/project1", true, cx)
 630        })
 631        .await
 632        .unwrap();
 633
 634    let worktree_id = cx.update(|cx| worktree.read(cx).id());
 635
 636    let buffer = project
 637        .update(cx, |project, cx| {
 638            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 639        })
 640        .await
 641        .unwrap();
 642
 643    let path = project
 644        .update(cx, |project, cx| {
 645            project.resolve_path_in_buffer("/code/project1/README.md", &buffer, cx)
 646        })
 647        .await
 648        .unwrap();
 649    assert!(path.is_file());
 650    assert_eq!(
 651        path.abs_path().unwrap().to_string_lossy(),
 652        "/code/project1/README.md"
 653    );
 654
 655    let path = project
 656        .update(cx, |project, cx| {
 657            project.resolve_path_in_buffer("../README.md", &buffer, cx)
 658        })
 659        .await
 660        .unwrap();
 661    assert!(path.is_file());
 662    assert_eq!(
 663        path.project_path().unwrap().clone(),
 664        ProjectPath::from((worktree_id, "README.md"))
 665    );
 666
 667    let path = project
 668        .update(cx, |project, cx| {
 669            project.resolve_path_in_buffer("../src", &buffer, cx)
 670        })
 671        .await
 672        .unwrap();
 673    assert_eq!(
 674        path.project_path().unwrap().clone(),
 675        ProjectPath::from((worktree_id, "src"))
 676    );
 677    assert!(path.is_dir());
 678}
 679
 680#[gpui::test]
 681async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 682    let fs = FakeFs::new(server_cx.executor());
 683    fs.insert_tree(
 684        "/code",
 685        json!({
 686            "project1": {
 687                ".git": {},
 688                "README.md": "# project 1",
 689                "src": {
 690                    "lib.rs": "fn one() -> usize { 1 }"
 691                }
 692            },
 693        }),
 694    )
 695    .await;
 696
 697    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 698
 699    let path = project
 700        .update(cx, |project, cx| {
 701            project.resolve_abs_path("/code/project1/README.md", cx)
 702        })
 703        .await
 704        .unwrap();
 705
 706    assert!(path.is_file());
 707    assert_eq!(
 708        path.abs_path().unwrap().to_string_lossy(),
 709        "/code/project1/README.md"
 710    );
 711
 712    let path = project
 713        .update(cx, |project, cx| {
 714            project.resolve_abs_path("/code/project1/src", cx)
 715        })
 716        .await
 717        .unwrap();
 718
 719    assert!(path.is_dir());
 720    assert_eq!(
 721        path.abs_path().unwrap().to_string_lossy(),
 722        "/code/project1/src"
 723    );
 724
 725    let path = project
 726        .update(cx, |project, cx| {
 727            project.resolve_abs_path("/code/project1/DOESNOTEXIST", cx)
 728        })
 729        .await;
 730    assert!(path.is_none());
 731}
 732
 733#[gpui::test(iterations = 10)]
 734async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 735    let fs = FakeFs::new(server_cx.executor());
 736    fs.insert_tree(
 737        "/code",
 738        json!({
 739            "project1": {
 740                ".git": {},
 741                "README.md": "# project 1",
 742                "src": {
 743                    "lib.rs": "fn one() -> usize { 1 }"
 744                }
 745            },
 746        }),
 747    )
 748    .await;
 749
 750    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 751    let (worktree, _) = project
 752        .update(cx, |project, cx| {
 753            project.find_or_create_worktree("/code/project1", true, cx)
 754        })
 755        .await
 756        .unwrap();
 757    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 758
 759    // Open a buffer on the client but cancel after a random amount of time.
 760    let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
 761    cx.executor().simulate_random_delay().await;
 762    drop(buffer);
 763
 764    // Try opening the same buffer again as the client, and ensure we can
 765    // still do it despite the cancellation above.
 766    let buffer = project
 767        .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
 768        .await
 769        .unwrap();
 770
 771    buffer.read_with(cx, |buf, _| {
 772        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
 773    });
 774}
 775
 776#[gpui::test]
 777async fn test_adding_then_removing_then_adding_worktrees(
 778    cx: &mut TestAppContext,
 779    server_cx: &mut TestAppContext,
 780) {
 781    let fs = FakeFs::new(server_cx.executor());
 782    fs.insert_tree(
 783        "/code",
 784        json!({
 785            "project1": {
 786                ".git": {},
 787                "README.md": "# project 1",
 788                "src": {
 789                    "lib.rs": "fn one() -> usize { 1 }"
 790                }
 791            },
 792            "project2": {
 793                "README.md": "# project 2",
 794            },
 795        }),
 796    )
 797    .await;
 798
 799    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 800    let (_worktree, _) = project
 801        .update(cx, |project, cx| {
 802            project.find_or_create_worktree("/code/project1", true, cx)
 803        })
 804        .await
 805        .unwrap();
 806
 807    let (worktree_2, _) = project
 808        .update(cx, |project, cx| {
 809            project.find_or_create_worktree("/code/project2", true, cx)
 810        })
 811        .await
 812        .unwrap();
 813    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
 814
 815    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
 816
 817    let (worktree_2, _) = project
 818        .update(cx, |project, cx| {
 819            project.find_or_create_worktree("/code/project2", true, cx)
 820        })
 821        .await
 822        .unwrap();
 823
 824    cx.run_until_parked();
 825    worktree_2.update(cx, |worktree, _cx| {
 826        assert!(worktree.is_visible());
 827        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
 828        assert_eq!(entries.len(), 2);
 829        assert_eq!(
 830            entries[1].path.to_string_lossy().to_string(),
 831            "README.md".to_string()
 832        )
 833    })
 834}
 835
 836#[gpui::test]
 837async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 838    let fs = FakeFs::new(server_cx.executor());
 839    fs.insert_tree(
 840        "/code",
 841        json!({
 842            "project1": {
 843                ".git": {},
 844                "README.md": "# project 1",
 845                "src": {
 846                    "lib.rs": "fn one() -> usize { 1 }"
 847                }
 848            },
 849        }),
 850    )
 851    .await;
 852
 853    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 854    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
 855    cx.executor().run_until_parked();
 856
 857    let buffer = buffer.await.unwrap();
 858
 859    cx.update(|cx| {
 860        assert_eq!(
 861            buffer.read(cx).text(),
 862            initial_server_settings_content().to_string()
 863        )
 864    })
 865}
 866
 867#[gpui::test(iterations = 20)]
 868async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 869    let fs = FakeFs::new(server_cx.executor());
 870    fs.insert_tree(
 871        "/code",
 872        json!({
 873            "project1": {
 874                ".git": {},
 875                "README.md": "# project 1",
 876                "src": {
 877                    "lib.rs": "fn one() -> usize { 1 }"
 878                }
 879            },
 880        }),
 881    )
 882    .await;
 883
 884    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 885
 886    let (worktree, _) = project
 887        .update(cx, |project, cx| {
 888            project.find_or_create_worktree("/code/project1", true, cx)
 889        })
 890        .await
 891        .unwrap();
 892
 893    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
 894    let buffer = project
 895        .update(cx, |project, cx| {
 896            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 897        })
 898        .await
 899        .unwrap();
 900
 901    buffer.update(cx, |buffer, cx| {
 902        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
 903        let ix = buffer.text().find('1').unwrap();
 904        buffer.edit([(ix..ix + 1, "100")], None, cx);
 905    });
 906
 907    let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
 908    client
 909        .update(cx, |client, cx| client.simulate_disconnect(cx))
 910        .detach();
 911
 912    project
 913        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 914        .await
 915        .unwrap();
 916
 917    assert_eq!(
 918        fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
 919        "fn one() -> usize { 100 }"
 920    );
 921}
 922
 923#[gpui::test]
 924async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 925    let fs = FakeFs::new(server_cx.executor());
 926    fs.insert_tree(
 927        "/code",
 928        json!({
 929            "project1": {
 930                ".git": {},
 931                "README.md": "# project 1",
 932            },
 933        }),
 934    )
 935    .await;
 936
 937    let (project, headless_project) = init_test(&fs, cx, server_cx).await;
 938    let branches = ["main", "dev", "feature-1"];
 939    fs.insert_branches(Path::new("/code/project1/.git"), &branches);
 940
 941    let (worktree, _) = project
 942        .update(cx, |project, cx| {
 943            project.find_or_create_worktree("/code/project1", true, cx)
 944        })
 945        .await
 946        .unwrap();
 947
 948    let worktree_id = cx.update(|cx| worktree.read(cx).id());
 949    let root_path = ProjectPath::root_path(worktree_id);
 950    // Give the worktree a bit of time to index the file system
 951    cx.run_until_parked();
 952
 953    let remote_branches = project
 954        .update(cx, |project, cx| project.branches(root_path.clone(), cx))
 955        .await
 956        .unwrap();
 957
 958    let new_branch = branches[2];
 959
 960    let remote_branches = remote_branches
 961        .into_iter()
 962        .map(|branch| branch.name)
 963        .collect::<Vec<_>>();
 964
 965    assert_eq!(&remote_branches, &branches);
 966
 967    cx.update(|cx| {
 968        project.update(cx, |project, cx| {
 969            project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
 970        })
 971    })
 972    .await
 973    .unwrap();
 974
 975    cx.run_until_parked();
 976
 977    let server_branch = server_cx.update(|cx| {
 978        headless_project.update(cx, |headless_project, cx| {
 979            headless_project
 980                .worktree_store
 981                .update(cx, |worktree_store, cx| {
 982                    worktree_store
 983                        .current_branch(root_path.clone(), cx)
 984                        .unwrap()
 985                })
 986        })
 987    });
 988
 989    assert_eq!(server_branch.as_ref(), branches[2]);
 990
 991    // Also try creating a new branch
 992    cx.update(|cx| {
 993        project.update(cx, |project, cx| {
 994            project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
 995        })
 996    })
 997    .await
 998    .unwrap();
 999
1000    cx.run_until_parked();
1001
1002    let server_branch = server_cx.update(|cx| {
1003        headless_project.update(cx, |headless_project, cx| {
1004            headless_project
1005                .worktree_store
1006                .update(cx, |worktree_store, cx| {
1007                    worktree_store.current_branch(root_path, cx).unwrap()
1008                })
1009        })
1010    });
1011
1012    assert_eq!(server_branch.as_ref(), "totally-new-branch");
1013}
1014
1015pub async fn init_test(
1016    server_fs: &Arc<FakeFs>,
1017    cx: &mut TestAppContext,
1018    server_cx: &mut TestAppContext,
1019) -> (Model<Project>, Model<HeadlessProject>) {
1020    let server_fs = server_fs.clone();
1021    init_logger();
1022
1023    let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
1024    let http_client = Arc::new(BlockedHttpClient);
1025    let node_runtime = NodeRuntime::unavailable();
1026    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1027    server_cx.update(HeadlessProject::init);
1028    let headless = server_cx.new_model(|cx| {
1029        client::init_settings(cx);
1030
1031        HeadlessProject::new(
1032            crate::HeadlessAppState {
1033                session: ssh_server_client,
1034                fs: server_fs.clone(),
1035                http_client,
1036                node_runtime,
1037                languages,
1038            },
1039            cx,
1040        )
1041    });
1042
1043    let ssh = SshRemoteClient::fake_client(opts, cx).await;
1044    let project = build_project(ssh, cx);
1045    project
1046        .update(cx, {
1047            let headless = headless.clone();
1048            |_, cx| cx.on_release(|_, _| drop(headless))
1049        })
1050        .detach();
1051    (project, headless)
1052}
1053
1054fn init_logger() {
1055    if std::env::var("RUST_LOG").is_ok() {
1056        env_logger::try_init().ok();
1057    }
1058}
1059
1060fn build_project(ssh: Model<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
1061    cx.update(|cx| {
1062        if !cx.has_global::<SettingsStore>() {
1063            let settings_store = SettingsStore::test(cx);
1064            cx.set_global(settings_store);
1065        }
1066    });
1067
1068    let client = cx.update(|cx| {
1069        Client::new(
1070            Arc::new(FakeSystemClock::default()),
1071            FakeHttpClient::with_404_response(),
1072            cx,
1073        )
1074    });
1075
1076    let node = NodeRuntime::unavailable();
1077    let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
1078    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1079    let fs = FakeFs::new(cx.executor());
1080
1081    cx.update(|cx| {
1082        Project::init(&client, cx);
1083        language::init(cx);
1084    });
1085
1086    cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1087}