remote_editing_tests.rs

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