remote_editing_tests.rs

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