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_file_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 608    let fs = FakeFs::new(server_cx.executor());
 609    fs.insert_tree(
 610        "/code",
 611        json!({
 612            "project1": {
 613                ".git": {},
 614                "README.md": "# project 1",
 615                "src": {
 616                    "lib.rs": "fn one() -> usize { 1 }"
 617                }
 618            },
 619        }),
 620    )
 621    .await;
 622
 623    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 624    let (worktree, _) = project
 625        .update(cx, |project, cx| {
 626            project.find_or_create_worktree("/code/project1", true, cx)
 627        })
 628        .await
 629        .unwrap();
 630
 631    let worktree_id = cx.update(|cx| worktree.read(cx).id());
 632
 633    let buffer = project
 634        .update(cx, |project, cx| {
 635            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 636        })
 637        .await
 638        .unwrap();
 639
 640    let path = project
 641        .update(cx, |project, cx| {
 642            project.resolve_existing_file_path("/code/project1/README.md", &buffer, cx)
 643        })
 644        .await
 645        .unwrap();
 646    assert_eq!(
 647        path.abs_path().unwrap().to_string_lossy(),
 648        "/code/project1/README.md"
 649    );
 650
 651    let path = project
 652        .update(cx, |project, cx| {
 653            project.resolve_existing_file_path("../README.md", &buffer, cx)
 654        })
 655        .await
 656        .unwrap();
 657
 658    assert_eq!(
 659        path.project_path().unwrap().clone(),
 660        ProjectPath::from((worktree_id, "README.md"))
 661    );
 662}
 663
 664#[gpui::test(iterations = 10)]
 665async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 666    let fs = FakeFs::new(server_cx.executor());
 667    fs.insert_tree(
 668        "/code",
 669        json!({
 670            "project1": {
 671                ".git": {},
 672                "README.md": "# project 1",
 673                "src": {
 674                    "lib.rs": "fn one() -> usize { 1 }"
 675                }
 676            },
 677        }),
 678    )
 679    .await;
 680
 681    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 682    let (worktree, _) = project
 683        .update(cx, |project, cx| {
 684            project.find_or_create_worktree("/code/project1", true, cx)
 685        })
 686        .await
 687        .unwrap();
 688    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 689
 690    // Open a buffer on the client but cancel after a random amount of time.
 691    let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
 692    cx.executor().simulate_random_delay().await;
 693    drop(buffer);
 694
 695    // Try opening the same buffer again as the client, and ensure we can
 696    // still do it despite the cancellation above.
 697    let buffer = project
 698        .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
 699        .await
 700        .unwrap();
 701
 702    buffer.read_with(cx, |buf, _| {
 703        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
 704    });
 705}
 706
 707#[gpui::test]
 708async fn test_adding_then_removing_then_adding_worktrees(
 709    cx: &mut TestAppContext,
 710    server_cx: &mut TestAppContext,
 711) {
 712    let fs = FakeFs::new(server_cx.executor());
 713    fs.insert_tree(
 714        "/code",
 715        json!({
 716            "project1": {
 717                ".git": {},
 718                "README.md": "# project 1",
 719                "src": {
 720                    "lib.rs": "fn one() -> usize { 1 }"
 721                }
 722            },
 723            "project2": {
 724                "README.md": "# project 2",
 725            },
 726        }),
 727    )
 728    .await;
 729
 730    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 731    let (_worktree, _) = project
 732        .update(cx, |project, cx| {
 733            project.find_or_create_worktree("/code/project1", true, cx)
 734        })
 735        .await
 736        .unwrap();
 737
 738    let (worktree_2, _) = project
 739        .update(cx, |project, cx| {
 740            project.find_or_create_worktree("/code/project2", true, cx)
 741        })
 742        .await
 743        .unwrap();
 744    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
 745
 746    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
 747
 748    let (worktree_2, _) = project
 749        .update(cx, |project, cx| {
 750            project.find_or_create_worktree("/code/project2", true, cx)
 751        })
 752        .await
 753        .unwrap();
 754
 755    cx.run_until_parked();
 756    worktree_2.update(cx, |worktree, _cx| {
 757        assert!(worktree.is_visible());
 758        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
 759        assert_eq!(entries.len(), 2);
 760        assert_eq!(
 761            entries[1].path.to_string_lossy().to_string(),
 762            "README.md".to_string()
 763        )
 764    })
 765}
 766
 767#[gpui::test]
 768async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 769    let fs = FakeFs::new(server_cx.executor());
 770    fs.insert_tree(
 771        "/code",
 772        json!({
 773            "project1": {
 774                ".git": {},
 775                "README.md": "# project 1",
 776                "src": {
 777                    "lib.rs": "fn one() -> usize { 1 }"
 778                }
 779            },
 780        }),
 781    )
 782    .await;
 783
 784    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 785    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
 786    cx.executor().run_until_parked();
 787
 788    let buffer = buffer.await.unwrap();
 789
 790    cx.update(|cx| {
 791        assert_eq!(
 792            buffer.read(cx).text(),
 793            initial_server_settings_content().to_string()
 794        )
 795    })
 796}
 797
 798#[gpui::test(iterations = 20)]
 799async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 800    let fs = FakeFs::new(server_cx.executor());
 801    fs.insert_tree(
 802        "/code",
 803        json!({
 804            "project1": {
 805                ".git": {},
 806                "README.md": "# project 1",
 807                "src": {
 808                    "lib.rs": "fn one() -> usize { 1 }"
 809                }
 810            },
 811        }),
 812    )
 813    .await;
 814
 815    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 816
 817    let (worktree, _) = project
 818        .update(cx, |project, cx| {
 819            project.find_or_create_worktree("/code/project1", true, cx)
 820        })
 821        .await
 822        .unwrap();
 823
 824    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
 825    let buffer = project
 826        .update(cx, |project, cx| {
 827            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 828        })
 829        .await
 830        .unwrap();
 831
 832    buffer.update(cx, |buffer, cx| {
 833        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
 834        let ix = buffer.text().find('1').unwrap();
 835        buffer.edit([(ix..ix + 1, "100")], None, cx);
 836    });
 837
 838    let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
 839    client
 840        .update(cx, |client, cx| client.simulate_disconnect(cx))
 841        .detach();
 842
 843    project
 844        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 845        .await
 846        .unwrap();
 847
 848    assert_eq!(
 849        fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
 850        "fn one() -> usize { 100 }"
 851    );
 852}
 853
 854#[gpui::test]
 855async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 856    let fs = FakeFs::new(server_cx.executor());
 857    fs.insert_tree(
 858        "/code",
 859        json!({
 860            "project1": {
 861                ".git": {},
 862                "README.md": "# project 1",
 863            },
 864        }),
 865    )
 866    .await;
 867
 868    let (project, headless_project) = init_test(&fs, cx, server_cx).await;
 869    let branches = ["main", "dev", "feature-1"];
 870    fs.insert_branches(Path::new("/code/project1/.git"), &branches);
 871
 872    let (worktree, _) = project
 873        .update(cx, |project, cx| {
 874            project.find_or_create_worktree("/code/project1", true, cx)
 875        })
 876        .await
 877        .unwrap();
 878
 879    let worktree_id = cx.update(|cx| worktree.read(cx).id());
 880    let root_path = ProjectPath::root_path(worktree_id);
 881    // Give the worktree a bit of time to index the file system
 882    cx.run_until_parked();
 883
 884    let remote_branches = project
 885        .update(cx, |project, cx| project.branches(root_path.clone(), cx))
 886        .await
 887        .unwrap();
 888
 889    let new_branch = branches[2];
 890
 891    let remote_branches = remote_branches
 892        .into_iter()
 893        .map(|branch| branch.name)
 894        .collect::<Vec<_>>();
 895
 896    assert_eq!(&remote_branches, &branches);
 897
 898    cx.update(|cx| {
 899        project.update(cx, |project, cx| {
 900            project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
 901        })
 902    })
 903    .await
 904    .unwrap();
 905
 906    cx.run_until_parked();
 907
 908    let server_branch = server_cx.update(|cx| {
 909        headless_project.update(cx, |headless_project, cx| {
 910            headless_project
 911                .worktree_store
 912                .update(cx, |worktree_store, cx| {
 913                    worktree_store
 914                        .current_branch(root_path.clone(), cx)
 915                        .unwrap()
 916                })
 917        })
 918    });
 919
 920    assert_eq!(server_branch.as_ref(), branches[2]);
 921
 922    // Also try creating a new branch
 923    cx.update(|cx| {
 924        project.update(cx, |project, cx| {
 925            project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
 926        })
 927    })
 928    .await
 929    .unwrap();
 930
 931    cx.run_until_parked();
 932
 933    let server_branch = server_cx.update(|cx| {
 934        headless_project.update(cx, |headless_project, cx| {
 935            headless_project
 936                .worktree_store
 937                .update(cx, |worktree_store, cx| {
 938                    worktree_store.current_branch(root_path, cx).unwrap()
 939                })
 940        })
 941    });
 942
 943    assert_eq!(server_branch.as_ref(), "totally-new-branch");
 944}
 945
 946pub async fn init_test(
 947    server_fs: &Arc<FakeFs>,
 948    cx: &mut TestAppContext,
 949    server_cx: &mut TestAppContext,
 950) -> (Model<Project>, Model<HeadlessProject>) {
 951    let server_fs = server_fs.clone();
 952    init_logger();
 953
 954    let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
 955    let http_client = Arc::new(BlockedHttpClient);
 956    let node_runtime = NodeRuntime::unavailable();
 957    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
 958    server_cx.update(HeadlessProject::init);
 959    let headless = server_cx.new_model(|cx| {
 960        client::init_settings(cx);
 961
 962        HeadlessProject::new(
 963            crate::HeadlessAppState {
 964                session: ssh_server_client,
 965                fs: server_fs.clone(),
 966                http_client,
 967                node_runtime,
 968                languages,
 969            },
 970            cx,
 971        )
 972    });
 973
 974    let ssh = SshRemoteClient::fake_client(opts, cx).await;
 975    let project = build_project(ssh, cx);
 976    project
 977        .update(cx, {
 978            let headless = headless.clone();
 979            |_, cx| cx.on_release(|_, _| drop(headless))
 980        })
 981        .detach();
 982    (project, headless)
 983}
 984
 985fn init_logger() {
 986    if std::env::var("RUST_LOG").is_ok() {
 987        env_logger::try_init().ok();
 988    }
 989}
 990
 991fn build_project(ssh: Model<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
 992    cx.update(|cx| {
 993        if !cx.has_global::<SettingsStore>() {
 994            let settings_store = SettingsStore::test(cx);
 995            cx.set_global(settings_store);
 996        }
 997    });
 998
 999    let client = cx.update(|cx| {
1000        Client::new(
1001            Arc::new(FakeSystemClock::default()),
1002            FakeHttpClient::with_404_response(),
1003            cx,
1004        )
1005    });
1006
1007    let node = NodeRuntime::unavailable();
1008    let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
1009    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1010    let fs = FakeFs::new(cx.executor());
1011
1012    cx.update(|cx| {
1013        Project::init(&client, cx);
1014        language::init(cx);
1015    });
1016
1017    cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1018}