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
 510            .unwrap()
 511            .into_iter()
 512            .map(|c| c.label.text)
 513            .collect::<Vec<_>>(),
 514        vec!["boop".to_string()]
 515    );
 516
 517    fake_lsp.handle_request::<lsp::request::Rename, _, _>(|_, _| async move {
 518        Ok(Some(lsp::WorkspaceEdit {
 519            changes: Some(
 520                [(
 521                    lsp::Url::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
 522                    vec![lsp::TextEdit::new(
 523                        lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
 524                        "two".to_string(),
 525                    )],
 526                )]
 527                .into_iter()
 528                .collect(),
 529            ),
 530            ..Default::default()
 531        }))
 532    });
 533
 534    project
 535        .update(cx, |project, cx| {
 536            project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
 537        })
 538        .await
 539        .unwrap();
 540
 541    cx.run_until_parked();
 542    buffer.update(cx, |buffer, _| {
 543        assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
 544    })
 545}
 546
 547#[gpui::test]
 548async fn test_remote_cancel_language_server_work(
 549    cx: &mut TestAppContext,
 550    server_cx: &mut TestAppContext,
 551) {
 552    let fs = FakeFs::new(server_cx.executor());
 553    fs.insert_tree(
 554        path!("/code"),
 555        json!({
 556            "project1": {
 557                ".git": {},
 558                "README.md": "# project 1",
 559                "src": {
 560                    "lib.rs": "fn one() -> usize { 1 }"
 561                }
 562            },
 563        }),
 564    )
 565    .await;
 566
 567    let (project, headless) = init_test(&fs, cx, server_cx).await;
 568
 569    fs.insert_tree(
 570        path!("/code/project1/.zed"),
 571        json!({
 572            "settings.json": r#"
 573          {
 574            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
 575            "lsp": {
 576              "rust-analyzer": {
 577                "binary": {
 578                  "path": "~/.cargo/bin/rust-analyzer"
 579                }
 580              }
 581            }
 582          }"#
 583        }),
 584    )
 585    .await;
 586
 587    cx.update_entity(&project, |project, _| {
 588        project.languages().register_test_language(LanguageConfig {
 589            name: "Rust".into(),
 590            matcher: LanguageMatcher {
 591                path_suffixes: vec!["rs".into()],
 592                ..Default::default()
 593            },
 594            ..Default::default()
 595        });
 596        project.languages().register_fake_lsp_adapter(
 597            "Rust",
 598            FakeLspAdapter {
 599                name: "rust-analyzer",
 600                ..Default::default()
 601            },
 602        )
 603    });
 604
 605    let mut fake_lsp = server_cx.update(|cx| {
 606        headless.read(cx).languages.register_fake_language_server(
 607            LanguageServerName("rust-analyzer".into()),
 608            Default::default(),
 609            None,
 610        )
 611    });
 612
 613    cx.run_until_parked();
 614
 615    let worktree_id = project
 616        .update(cx, |project, cx| {
 617            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 618        })
 619        .await
 620        .unwrap()
 621        .0
 622        .read_with(cx, |worktree, _| worktree.id());
 623
 624    cx.run_until_parked();
 625
 626    let (buffer, _handle) = project
 627        .update(cx, |project, cx| {
 628            project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
 629        })
 630        .await
 631        .unwrap();
 632
 633    cx.run_until_parked();
 634
 635    let mut fake_lsp = fake_lsp.next().await.unwrap();
 636
 637    // Cancelling all language server work for a given buffer
 638    {
 639        // Two operations, one cancellable and one not.
 640        fake_lsp
 641            .start_progress_with(
 642                "another-token",
 643                lsp::WorkDoneProgressBegin {
 644                    cancellable: Some(false),
 645                    ..Default::default()
 646                },
 647            )
 648            .await;
 649
 650        let progress_token = "the-progress-token";
 651        fake_lsp
 652            .start_progress_with(
 653                progress_token,
 654                lsp::WorkDoneProgressBegin {
 655                    cancellable: Some(true),
 656                    ..Default::default()
 657                },
 658            )
 659            .await;
 660
 661        cx.executor().run_until_parked();
 662
 663        project.update(cx, |project, cx| {
 664            project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
 665        });
 666
 667        cx.executor().run_until_parked();
 668
 669        // Verify the cancellation was received on the server side
 670        let cancel_notification = fake_lsp
 671            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
 672            .await;
 673        assert_eq!(
 674            cancel_notification.token,
 675            lsp::NumberOrString::String(progress_token.into())
 676        );
 677    }
 678
 679    // Cancelling work by server_id and token
 680    {
 681        let server_id = fake_lsp.server.server_id();
 682        let progress_token = "the-progress-token";
 683
 684        fake_lsp
 685            .start_progress_with(
 686                progress_token,
 687                lsp::WorkDoneProgressBegin {
 688                    cancellable: Some(true),
 689                    ..Default::default()
 690                },
 691            )
 692            .await;
 693
 694        cx.executor().run_until_parked();
 695
 696        project.update(cx, |project, cx| {
 697            project.cancel_language_server_work(server_id, Some(progress_token.into()), cx)
 698        });
 699
 700        cx.executor().run_until_parked();
 701
 702        // Verify the cancellation was received on the server side
 703        let cancel_notification = fake_lsp
 704            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
 705            .await;
 706        assert_eq!(
 707            cancel_notification.token,
 708            lsp::NumberOrString::String(progress_token.into())
 709        );
 710    }
 711}
 712
 713#[gpui::test]
 714async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 715    let fs = FakeFs::new(server_cx.executor());
 716    fs.insert_tree(
 717        path!("/code"),
 718        json!({
 719            "project1": {
 720                ".git": {},
 721                "README.md": "# project 1",
 722                "src": {
 723                    "lib.rs": "fn one() -> usize { 1 }"
 724                }
 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(path!("/code/project1"), true, cx)
 734        })
 735        .await
 736        .unwrap();
 737
 738    let worktree_id = cx.update(|cx| worktree.read(cx).id());
 739
 740    let buffer = project
 741        .update(cx, |project, cx| {
 742            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 743        })
 744        .await
 745        .unwrap();
 746
 747    fs.save(
 748        &PathBuf::from(path!("/code/project1/src/lib.rs")),
 749        &("bangles".to_string().into()),
 750        LineEnding::Unix,
 751    )
 752    .await
 753    .unwrap();
 754
 755    cx.run_until_parked();
 756
 757    buffer.update(cx, |buffer, cx| {
 758        assert_eq!(buffer.text(), "bangles");
 759        buffer.edit([(0..0, "a")], None, cx);
 760    });
 761
 762    fs.save(
 763        &PathBuf::from(path!("/code/project1/src/lib.rs")),
 764        &("bloop".to_string().into()),
 765        LineEnding::Unix,
 766    )
 767    .await
 768    .unwrap();
 769
 770    cx.run_until_parked();
 771    cx.update(|cx| {
 772        assert!(buffer.read(cx).has_conflict());
 773    });
 774
 775    project
 776        .update(cx, |project, cx| {
 777            project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
 778        })
 779        .await
 780        .unwrap();
 781    cx.run_until_parked();
 782
 783    cx.update(|cx| {
 784        assert!(!buffer.read(cx).has_conflict());
 785    });
 786}
 787
 788#[gpui::test]
 789async fn test_remote_resolve_path_in_buffer(
 790    cx: &mut TestAppContext,
 791    server_cx: &mut TestAppContext,
 792) {
 793    let fs = FakeFs::new(server_cx.executor());
 794    // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
 795    fs.insert_tree(
 796        path!("/code"),
 797        json!({
 798            "project1": {
 799                ".git": {},
 800                "README.md": "# project 1",
 801                "src": {
 802                    "lib.rs": "fn one() -> usize { 1 }"
 803                }
 804            },
 805            "project2": {
 806                ".git": {},
 807                "README.md": "# project 2",
 808                "src": {
 809                    "lib.rs": "fn two() -> usize { 2 }"
 810                }
 811            }
 812        }),
 813    )
 814    .await;
 815
 816    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 817
 818    let _ = project
 819        .update(cx, |project, cx| {
 820            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 821        })
 822        .await
 823        .unwrap();
 824
 825    let (worktree2, _) = project
 826        .update(cx, |project, cx| {
 827            project.find_or_create_worktree(path!("/code/project2"), true, cx)
 828        })
 829        .await
 830        .unwrap();
 831
 832    let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
 833
 834    let buffer2 = project
 835        .update(cx, |project, cx| {
 836            project.open_buffer((worktree2_id, Path::new("src/lib.rs")), cx)
 837        })
 838        .await
 839        .unwrap();
 840
 841    let path = project
 842        .update(cx, |project, cx| {
 843            project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
 844        })
 845        .await
 846        .unwrap();
 847    assert!(path.is_file());
 848    assert_eq!(
 849        path.abs_path().unwrap().to_string_lossy(),
 850        path!("/code/project2/README.md")
 851    );
 852
 853    let path = project
 854        .update(cx, |project, cx| {
 855            project.resolve_path_in_buffer("../README.md", &buffer2, cx)
 856        })
 857        .await
 858        .unwrap();
 859    assert!(path.is_file());
 860    assert_eq!(
 861        path.project_path().unwrap().clone(),
 862        ProjectPath::from((worktree2_id, "README.md"))
 863    );
 864
 865    let path = project
 866        .update(cx, |project, cx| {
 867            project.resolve_path_in_buffer("../src", &buffer2, cx)
 868        })
 869        .await
 870        .unwrap();
 871    assert_eq!(
 872        path.project_path().unwrap().clone(),
 873        ProjectPath::from((worktree2_id, "src"))
 874    );
 875    assert!(path.is_dir());
 876}
 877
 878#[gpui::test]
 879async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 880    let fs = FakeFs::new(server_cx.executor());
 881    fs.insert_tree(
 882        path!("/code"),
 883        json!({
 884            "project1": {
 885                ".git": {},
 886                "README.md": "# project 1",
 887                "src": {
 888                    "lib.rs": "fn one() -> usize { 1 }"
 889                }
 890            },
 891        }),
 892    )
 893    .await;
 894
 895    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 896
 897    let path = project
 898        .update(cx, |project, cx| {
 899            project.resolve_abs_path(path!("/code/project1/README.md"), cx)
 900        })
 901        .await
 902        .unwrap();
 903
 904    assert!(path.is_file());
 905    assert_eq!(
 906        path.abs_path().unwrap().to_string_lossy(),
 907        path!("/code/project1/README.md")
 908    );
 909
 910    let path = project
 911        .update(cx, |project, cx| {
 912            project.resolve_abs_path(path!("/code/project1/src"), cx)
 913        })
 914        .await
 915        .unwrap();
 916
 917    assert!(path.is_dir());
 918    assert_eq!(
 919        path.abs_path().unwrap().to_string_lossy(),
 920        path!("/code/project1/src")
 921    );
 922
 923    let path = project
 924        .update(cx, |project, cx| {
 925            project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
 926        })
 927        .await;
 928    assert!(path.is_none());
 929}
 930
 931#[gpui::test(iterations = 10)]
 932async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 933    let fs = FakeFs::new(server_cx.executor());
 934    fs.insert_tree(
 935        "/code",
 936        json!({
 937            "project1": {
 938                ".git": {},
 939                "README.md": "# project 1",
 940                "src": {
 941                    "lib.rs": "fn one() -> usize { 1 }"
 942                }
 943            },
 944        }),
 945    )
 946    .await;
 947
 948    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 949    let (worktree, _) = project
 950        .update(cx, |project, cx| {
 951            project.find_or_create_worktree("/code/project1", true, cx)
 952        })
 953        .await
 954        .unwrap();
 955    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 956
 957    // Open a buffer on the client but cancel after a random amount of time.
 958    let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
 959    cx.executor().simulate_random_delay().await;
 960    drop(buffer);
 961
 962    // Try opening the same buffer again as the client, and ensure we can
 963    // still do it despite the cancellation above.
 964    let buffer = project
 965        .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
 966        .await
 967        .unwrap();
 968
 969    buffer.read_with(cx, |buf, _| {
 970        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
 971    });
 972}
 973
 974#[gpui::test]
 975async fn test_adding_then_removing_then_adding_worktrees(
 976    cx: &mut TestAppContext,
 977    server_cx: &mut TestAppContext,
 978) {
 979    let fs = FakeFs::new(server_cx.executor());
 980    fs.insert_tree(
 981        path!("/code"),
 982        json!({
 983            "project1": {
 984                ".git": {},
 985                "README.md": "# project 1",
 986                "src": {
 987                    "lib.rs": "fn one() -> usize { 1 }"
 988                }
 989            },
 990            "project2": {
 991                "README.md": "# project 2",
 992            },
 993        }),
 994    )
 995    .await;
 996
 997    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 998    let (_worktree, _) = project
 999        .update(cx, |project, cx| {
1000            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1001        })
1002        .await
1003        .unwrap();
1004
1005    let (worktree_2, _) = project
1006        .update(cx, |project, cx| {
1007            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1008        })
1009        .await
1010        .unwrap();
1011    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1012
1013    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1014
1015    let (worktree_2, _) = project
1016        .update(cx, |project, cx| {
1017            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1018        })
1019        .await
1020        .unwrap();
1021
1022    cx.run_until_parked();
1023    worktree_2.update(cx, |worktree, _cx| {
1024        assert!(worktree.is_visible());
1025        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1026        assert_eq!(entries.len(), 2);
1027        assert_eq!(
1028            entries[1].path.to_string_lossy().to_string(),
1029            "README.md".to_string()
1030        )
1031    })
1032}
1033
1034#[gpui::test]
1035async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1036    let fs = FakeFs::new(server_cx.executor());
1037    fs.insert_tree(
1038        path!("/code"),
1039        json!({
1040            "project1": {
1041                ".git": {},
1042                "README.md": "# project 1",
1043                "src": {
1044                    "lib.rs": "fn one() -> usize { 1 }"
1045                }
1046            },
1047        }),
1048    )
1049    .await;
1050
1051    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1052    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1053    cx.executor().run_until_parked();
1054
1055    let buffer = buffer.await.unwrap();
1056
1057    cx.update(|cx| {
1058        assert_eq!(
1059            buffer.read(cx).text(),
1060            initial_server_settings_content()
1061                .to_string()
1062                .replace("\r\n", "\n")
1063        )
1064    })
1065}
1066
1067#[gpui::test(iterations = 20)]
1068async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1069    let fs = FakeFs::new(server_cx.executor());
1070    fs.insert_tree(
1071        path!("/code"),
1072        json!({
1073            "project1": {
1074                ".git": {},
1075                "README.md": "# project 1",
1076                "src": {
1077                    "lib.rs": "fn one() -> usize { 1 }"
1078                }
1079            },
1080        }),
1081    )
1082    .await;
1083
1084    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1085
1086    let (worktree, _) = project
1087        .update(cx, |project, cx| {
1088            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1089        })
1090        .await
1091        .unwrap();
1092
1093    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1094    let buffer = project
1095        .update(cx, |project, cx| {
1096            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1097        })
1098        .await
1099        .unwrap();
1100
1101    buffer.update(cx, |buffer, cx| {
1102        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1103        let ix = buffer.text().find('1').unwrap();
1104        buffer.edit([(ix..ix + 1, "100")], None, cx);
1105    });
1106
1107    let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
1108    client
1109        .update(cx, |client, cx| client.simulate_disconnect(cx))
1110        .detach();
1111
1112    project
1113        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1114        .await
1115        .unwrap();
1116
1117    assert_eq!(
1118        fs.load(path!("/code/project1/src/lib.rs").as_ref())
1119            .await
1120            .unwrap(),
1121        "fn one() -> usize { 100 }"
1122    );
1123}
1124
1125#[gpui::test]
1126async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1127    let fs = FakeFs::new(server_cx.executor());
1128    fs.insert_tree(
1129        "/code",
1130        json!({
1131            "project1": {
1132                ".git": {},
1133                "README.md": "# project 1",
1134            },
1135        }),
1136    )
1137    .await;
1138
1139    let (project, _) = init_test(&fs, cx, server_cx).await;
1140
1141    let (worktree, _) = project
1142        .update(cx, |project, cx| {
1143            project.find_or_create_worktree("/code/project1", true, cx)
1144        })
1145        .await
1146        .unwrap();
1147
1148    cx.run_until_parked();
1149
1150    fs.rename(
1151        &PathBuf::from("/code/project1"),
1152        &PathBuf::from("/code/project2"),
1153        Default::default(),
1154    )
1155    .await
1156    .unwrap();
1157
1158    cx.run_until_parked();
1159    worktree.update(cx, |worktree, _| {
1160        assert_eq!(worktree.root_name(), "project2")
1161    })
1162}
1163
1164#[gpui::test]
1165async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1166    let fs = FakeFs::new(server_cx.executor());
1167    fs.insert_tree(
1168        "/code",
1169        json!({
1170            "project1": {
1171                ".git": {},
1172                "README.md": "# project 1",
1173            },
1174        }),
1175    )
1176    .await;
1177
1178    let (project, _) = init_test(&fs, cx, server_cx).await;
1179    let (worktree, _) = project
1180        .update(cx, |project, cx| {
1181            project.find_or_create_worktree("/code/project1", true, cx)
1182        })
1183        .await
1184        .unwrap();
1185
1186    cx.run_until_parked();
1187
1188    let entry = worktree
1189        .update(cx, |worktree, cx| {
1190            let entry = worktree.entry_for_path("README.md").unwrap();
1191            worktree.rename_entry(entry.id, Path::new("README.rst"), cx)
1192        })
1193        .await
1194        .unwrap()
1195        .to_included()
1196        .unwrap();
1197
1198    cx.run_until_parked();
1199
1200    worktree.update(cx, |worktree, _| {
1201        assert_eq!(worktree.entry_for_path("README.rst").unwrap().id, entry.id)
1202    });
1203}
1204
1205#[gpui::test]
1206async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1207    let text_2 = "
1208        fn one() -> usize {
1209            1
1210        }
1211    "
1212    .unindent();
1213    let text_1 = "
1214        fn one() -> usize {
1215            0
1216        }
1217    "
1218    .unindent();
1219
1220    let fs = FakeFs::new(server_cx.executor());
1221    fs.insert_tree(
1222        "/code",
1223        json!({
1224            "project1": {
1225                ".git": {},
1226                "src": {
1227                    "lib.rs": text_2
1228                },
1229                "README.md": "# project 1",
1230            },
1231        }),
1232    )
1233    .await;
1234    fs.set_index_for_repo(
1235        Path::new("/code/project1/.git"),
1236        &[("src/lib.rs".into(), text_1.clone())],
1237    );
1238    fs.set_head_for_repo(
1239        Path::new("/code/project1/.git"),
1240        &[("src/lib.rs".into(), text_1.clone())],
1241    );
1242
1243    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1244    let (worktree, _) = project
1245        .update(cx, |project, cx| {
1246            project.find_or_create_worktree("/code/project1", true, cx)
1247        })
1248        .await
1249        .unwrap();
1250    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1251    cx.executor().run_until_parked();
1252
1253    let buffer = project
1254        .update(cx, |project, cx| {
1255            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1256        })
1257        .await
1258        .unwrap();
1259    let diff = project
1260        .update(cx, |project, cx| {
1261            project.open_uncommitted_diff(buffer.clone(), cx)
1262        })
1263        .await
1264        .unwrap();
1265
1266    diff.read_with(cx, |diff, cx| {
1267        assert_eq!(diff.base_text_string().unwrap(), text_1);
1268        assert_eq!(
1269            diff.secondary_diff()
1270                .unwrap()
1271                .read(cx)
1272                .base_text_string()
1273                .unwrap(),
1274            text_1
1275        );
1276    });
1277
1278    // stage the current buffer's contents
1279    fs.set_index_for_repo(
1280        Path::new("/code/project1/.git"),
1281        &[("src/lib.rs".into(), text_2.clone())],
1282    );
1283
1284    cx.executor().run_until_parked();
1285    diff.read_with(cx, |diff, cx| {
1286        assert_eq!(diff.base_text_string().unwrap(), text_1);
1287        assert_eq!(
1288            diff.secondary_diff()
1289                .unwrap()
1290                .read(cx)
1291                .base_text_string()
1292                .unwrap(),
1293            text_2
1294        );
1295    });
1296
1297    // commit the current buffer's contents
1298    fs.set_head_for_repo(
1299        Path::new("/code/project1/.git"),
1300        &[("src/lib.rs".into(), text_2.clone())],
1301    );
1302
1303    cx.executor().run_until_parked();
1304    diff.read_with(cx, |diff, cx| {
1305        assert_eq!(diff.base_text_string().unwrap(), text_2);
1306        assert_eq!(
1307            diff.secondary_diff()
1308                .unwrap()
1309                .read(cx)
1310                .base_text_string()
1311                .unwrap(),
1312            text_2
1313        );
1314    });
1315}
1316
1317#[gpui::test]
1318async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1319    let fs = FakeFs::new(server_cx.executor());
1320    fs.insert_tree(
1321        path!("/code"),
1322        json!({
1323            "project1": {
1324                ".git": {},
1325                "README.md": "# project 1",
1326            },
1327        }),
1328    )
1329    .await;
1330
1331    let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1332    let branches = ["main", "dev", "feature-1"];
1333    let branches_set = branches
1334        .iter()
1335        .map(ToString::to_string)
1336        .collect::<HashSet<_>>();
1337    fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1338
1339    let (_worktree, _) = project
1340        .update(cx, |project, cx| {
1341            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1342        })
1343        .await
1344        .unwrap();
1345    // Give the worktree a bit of time to index the file system
1346    cx.run_until_parked();
1347
1348    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1349
1350    let remote_branches = repository
1351        .update(cx, |repository, _| repository.branches())
1352        .await
1353        .unwrap()
1354        .unwrap();
1355
1356    let new_branch = branches[2];
1357
1358    let remote_branches = remote_branches
1359        .into_iter()
1360        .map(|branch| branch.name.to_string())
1361        .collect::<HashSet<_>>();
1362
1363    assert_eq!(&remote_branches, &branches_set);
1364
1365    cx.update(|cx| repository.read(cx).change_branch(new_branch.to_string()))
1366        .await
1367        .unwrap()
1368        .unwrap();
1369
1370    cx.run_until_parked();
1371
1372    let server_branch = server_cx.update(|cx| {
1373        headless_project.update(cx, |headless_project, cx| {
1374            headless_project.git_store.update(cx, |git_store, cx| {
1375                git_store
1376                    .repositories()
1377                    .values()
1378                    .next()
1379                    .unwrap()
1380                    .read(cx)
1381                    .current_branch()
1382                    .unwrap()
1383                    .clone()
1384            })
1385        })
1386    });
1387
1388    assert_eq!(server_branch.name, branches[2]);
1389
1390    // Also try creating a new branch
1391    cx.update(|cx| {
1392        repository
1393            .read(cx)
1394            .create_branch("totally-new-branch".to_string())
1395    })
1396    .await
1397    .unwrap()
1398    .unwrap();
1399
1400    cx.update(|cx| {
1401        repository
1402            .read(cx)
1403            .change_branch("totally-new-branch".to_string())
1404    })
1405    .await
1406    .unwrap()
1407    .unwrap();
1408
1409    cx.run_until_parked();
1410
1411    let server_branch = server_cx.update(|cx| {
1412        headless_project.update(cx, |headless_project, cx| {
1413            headless_project.git_store.update(cx, |git_store, cx| {
1414                git_store
1415                    .repositories()
1416                    .values()
1417                    .next()
1418                    .unwrap()
1419                    .read(cx)
1420                    .current_branch()
1421                    .unwrap()
1422                    .clone()
1423            })
1424        })
1425    });
1426
1427    assert_eq!(server_branch.name, "totally-new-branch");
1428}
1429
1430pub async fn init_test(
1431    server_fs: &Arc<FakeFs>,
1432    cx: &mut TestAppContext,
1433    server_cx: &mut TestAppContext,
1434) -> (Entity<Project>, Entity<HeadlessProject>) {
1435    let server_fs = server_fs.clone();
1436    cx.update(|cx| {
1437        release_channel::init(SemanticVersion::default(), cx);
1438    });
1439    server_cx.update(|cx| {
1440        release_channel::init(SemanticVersion::default(), cx);
1441    });
1442    init_logger();
1443
1444    let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
1445    let http_client = Arc::new(BlockedHttpClient);
1446    let node_runtime = NodeRuntime::unavailable();
1447    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1448    let proxy = Arc::new(ExtensionHostProxy::new());
1449    server_cx.update(HeadlessProject::init);
1450    let headless = server_cx.new(|cx| {
1451        client::init_settings(cx);
1452
1453        HeadlessProject::new(
1454            crate::HeadlessAppState {
1455                session: ssh_server_client,
1456                fs: server_fs.clone(),
1457                http_client,
1458                node_runtime,
1459                languages,
1460                extension_host_proxy: proxy,
1461            },
1462            cx,
1463        )
1464    });
1465
1466    let ssh = SshRemoteClient::fake_client(opts, cx).await;
1467    let project = build_project(ssh, cx);
1468    project
1469        .update(cx, {
1470            let headless = headless.clone();
1471            |_, cx| cx.on_release(|_, _| drop(headless))
1472        })
1473        .detach();
1474    (project, headless)
1475}
1476
1477fn init_logger() {
1478    if std::env::var("RUST_LOG").is_ok() {
1479        env_logger::try_init().ok();
1480    }
1481}
1482
1483fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1484    cx.update(|cx| {
1485        if !cx.has_global::<SettingsStore>() {
1486            let settings_store = SettingsStore::test(cx);
1487            cx.set_global(settings_store);
1488        }
1489    });
1490
1491    let client = cx.update(|cx| {
1492        Client::new(
1493            Arc::new(FakeSystemClock::new()),
1494            FakeHttpClient::with_404_response(),
1495            cx,
1496        )
1497    });
1498
1499    let node = NodeRuntime::unavailable();
1500    let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1501    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1502    let fs = FakeFs::new(cx.executor());
1503
1504    cx.update(|cx| {
1505        Project::init(&client, cx);
1506        language::init(cx);
1507    });
1508
1509    cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1510}