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    fs.insert_tree(
 791        path!("/code"),
 792        json!({
 793            "project1": {
 794                ".git": {},
 795                "README.md": "# project 1",
 796                "src": {
 797                    "lib.rs": "fn one() -> usize { 1 }"
 798                }
 799            },
 800        }),
 801    )
 802    .await;
 803
 804    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 805    let (worktree, _) = project
 806        .update(cx, |project, cx| {
 807            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 808        })
 809        .await
 810        .unwrap();
 811
 812    let worktree_id = cx.update(|cx| worktree.read(cx).id());
 813
 814    let buffer = project
 815        .update(cx, |project, cx| {
 816            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 817        })
 818        .await
 819        .unwrap();
 820
 821    let path = project
 822        .update(cx, |project, cx| {
 823            project.resolve_path_in_buffer(path!("/code/project1/README.md"), &buffer, cx)
 824        })
 825        .await
 826        .unwrap();
 827    assert!(path.is_file());
 828    assert_eq!(
 829        path.abs_path().unwrap().to_string_lossy(),
 830        path!("/code/project1/README.md")
 831    );
 832
 833    let path = project
 834        .update(cx, |project, cx| {
 835            project.resolve_path_in_buffer("../README.md", &buffer, cx)
 836        })
 837        .await
 838        .unwrap();
 839    assert!(path.is_file());
 840    assert_eq!(
 841        path.project_path().unwrap().clone(),
 842        ProjectPath::from((worktree_id, "README.md"))
 843    );
 844
 845    let path = project
 846        .update(cx, |project, cx| {
 847            project.resolve_path_in_buffer("../src", &buffer, cx)
 848        })
 849        .await
 850        .unwrap();
 851    assert_eq!(
 852        path.project_path().unwrap().clone(),
 853        ProjectPath::from((worktree_id, "src"))
 854    );
 855    assert!(path.is_dir());
 856}
 857
 858#[gpui::test]
 859async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 860    let fs = FakeFs::new(server_cx.executor());
 861    fs.insert_tree(
 862        path!("/code"),
 863        json!({
 864            "project1": {
 865                ".git": {},
 866                "README.md": "# project 1",
 867                "src": {
 868                    "lib.rs": "fn one() -> usize { 1 }"
 869                }
 870            },
 871        }),
 872    )
 873    .await;
 874
 875    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 876
 877    let path = project
 878        .update(cx, |project, cx| {
 879            project.resolve_abs_path(path!("/code/project1/README.md"), cx)
 880        })
 881        .await
 882        .unwrap();
 883
 884    assert!(path.is_file());
 885    assert_eq!(
 886        path.abs_path().unwrap().to_string_lossy(),
 887        path!("/code/project1/README.md")
 888    );
 889
 890    let path = project
 891        .update(cx, |project, cx| {
 892            project.resolve_abs_path(path!("/code/project1/src"), cx)
 893        })
 894        .await
 895        .unwrap();
 896
 897    assert!(path.is_dir());
 898    assert_eq!(
 899        path.abs_path().unwrap().to_string_lossy(),
 900        path!("/code/project1/src")
 901    );
 902
 903    let path = project
 904        .update(cx, |project, cx| {
 905            project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
 906        })
 907        .await;
 908    assert!(path.is_none());
 909}
 910
 911#[gpui::test(iterations = 10)]
 912async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 913    let fs = FakeFs::new(server_cx.executor());
 914    fs.insert_tree(
 915        "/code",
 916        json!({
 917            "project1": {
 918                ".git": {},
 919                "README.md": "# project 1",
 920                "src": {
 921                    "lib.rs": "fn one() -> usize { 1 }"
 922                }
 923            },
 924        }),
 925    )
 926    .await;
 927
 928    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 929    let (worktree, _) = project
 930        .update(cx, |project, cx| {
 931            project.find_or_create_worktree("/code/project1", true, cx)
 932        })
 933        .await
 934        .unwrap();
 935    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 936
 937    // Open a buffer on the client but cancel after a random amount of time.
 938    let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
 939    cx.executor().simulate_random_delay().await;
 940    drop(buffer);
 941
 942    // Try opening the same buffer again as the client, and ensure we can
 943    // still do it despite the cancellation above.
 944    let buffer = project
 945        .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
 946        .await
 947        .unwrap();
 948
 949    buffer.read_with(cx, |buf, _| {
 950        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
 951    });
 952}
 953
 954#[gpui::test]
 955async fn test_adding_then_removing_then_adding_worktrees(
 956    cx: &mut TestAppContext,
 957    server_cx: &mut TestAppContext,
 958) {
 959    let fs = FakeFs::new(server_cx.executor());
 960    fs.insert_tree(
 961        path!("/code"),
 962        json!({
 963            "project1": {
 964                ".git": {},
 965                "README.md": "# project 1",
 966                "src": {
 967                    "lib.rs": "fn one() -> usize { 1 }"
 968                }
 969            },
 970            "project2": {
 971                "README.md": "# project 2",
 972            },
 973        }),
 974    )
 975    .await;
 976
 977    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 978    let (_worktree, _) = project
 979        .update(cx, |project, cx| {
 980            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 981        })
 982        .await
 983        .unwrap();
 984
 985    let (worktree_2, _) = project
 986        .update(cx, |project, cx| {
 987            project.find_or_create_worktree(path!("/code/project2"), true, cx)
 988        })
 989        .await
 990        .unwrap();
 991    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
 992
 993    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
 994
 995    let (worktree_2, _) = project
 996        .update(cx, |project, cx| {
 997            project.find_or_create_worktree(path!("/code/project2"), true, cx)
 998        })
 999        .await
1000        .unwrap();
1001
1002    cx.run_until_parked();
1003    worktree_2.update(cx, |worktree, _cx| {
1004        assert!(worktree.is_visible());
1005        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1006        assert_eq!(entries.len(), 2);
1007        assert_eq!(
1008            entries[1].path.to_string_lossy().to_string(),
1009            "README.md".to_string()
1010        )
1011    })
1012}
1013
1014#[gpui::test]
1015async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1016    let fs = FakeFs::new(server_cx.executor());
1017    fs.insert_tree(
1018        path!("/code"),
1019        json!({
1020            "project1": {
1021                ".git": {},
1022                "README.md": "# project 1",
1023                "src": {
1024                    "lib.rs": "fn one() -> usize { 1 }"
1025                }
1026            },
1027        }),
1028    )
1029    .await;
1030
1031    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1032    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1033    cx.executor().run_until_parked();
1034
1035    let buffer = buffer.await.unwrap();
1036
1037    cx.update(|cx| {
1038        assert_eq!(
1039            buffer.read(cx).text(),
1040            initial_server_settings_content()
1041                .to_string()
1042                .replace("\r\n", "\n")
1043        )
1044    })
1045}
1046
1047#[gpui::test(iterations = 20)]
1048async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1049    let fs = FakeFs::new(server_cx.executor());
1050    fs.insert_tree(
1051        path!("/code"),
1052        json!({
1053            "project1": {
1054                ".git": {},
1055                "README.md": "# project 1",
1056                "src": {
1057                    "lib.rs": "fn one() -> usize { 1 }"
1058                }
1059            },
1060        }),
1061    )
1062    .await;
1063
1064    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1065
1066    let (worktree, _) = project
1067        .update(cx, |project, cx| {
1068            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1069        })
1070        .await
1071        .unwrap();
1072
1073    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1074    let buffer = project
1075        .update(cx, |project, cx| {
1076            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1077        })
1078        .await
1079        .unwrap();
1080
1081    buffer.update(cx, |buffer, cx| {
1082        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1083        let ix = buffer.text().find('1').unwrap();
1084        buffer.edit([(ix..ix + 1, "100")], None, cx);
1085    });
1086
1087    let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
1088    client
1089        .update(cx, |client, cx| client.simulate_disconnect(cx))
1090        .detach();
1091
1092    project
1093        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1094        .await
1095        .unwrap();
1096
1097    assert_eq!(
1098        fs.load(path!("/code/project1/src/lib.rs").as_ref())
1099            .await
1100            .unwrap(),
1101        "fn one() -> usize { 100 }"
1102    );
1103}
1104
1105#[gpui::test]
1106async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1107    let fs = FakeFs::new(server_cx.executor());
1108    fs.insert_tree(
1109        "/code",
1110        json!({
1111            "project1": {
1112                ".git": {},
1113                "README.md": "# project 1",
1114            },
1115        }),
1116    )
1117    .await;
1118
1119    let (project, _) = init_test(&fs, cx, server_cx).await;
1120
1121    let (worktree, _) = project
1122        .update(cx, |project, cx| {
1123            project.find_or_create_worktree("/code/project1", true, cx)
1124        })
1125        .await
1126        .unwrap();
1127
1128    cx.run_until_parked();
1129
1130    fs.rename(
1131        &PathBuf::from("/code/project1"),
1132        &PathBuf::from("/code/project2"),
1133        Default::default(),
1134    )
1135    .await
1136    .unwrap();
1137
1138    cx.run_until_parked();
1139    worktree.update(cx, |worktree, _| {
1140        assert_eq!(worktree.root_name(), "project2")
1141    })
1142}
1143
1144#[gpui::test]
1145async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1146    let fs = FakeFs::new(server_cx.executor());
1147    fs.insert_tree(
1148        "/code",
1149        json!({
1150            "project1": {
1151                ".git": {},
1152                "README.md": "# project 1",
1153            },
1154        }),
1155    )
1156    .await;
1157
1158    let (project, _) = init_test(&fs, cx, server_cx).await;
1159    let (worktree, _) = project
1160        .update(cx, |project, cx| {
1161            project.find_or_create_worktree("/code/project1", true, cx)
1162        })
1163        .await
1164        .unwrap();
1165
1166    cx.run_until_parked();
1167
1168    let entry = worktree
1169        .update(cx, |worktree, cx| {
1170            let entry = worktree.entry_for_path("README.md").unwrap();
1171            worktree.rename_entry(entry.id, Path::new("README.rst"), cx)
1172        })
1173        .await
1174        .unwrap()
1175        .to_included()
1176        .unwrap();
1177
1178    cx.run_until_parked();
1179
1180    worktree.update(cx, |worktree, _| {
1181        assert_eq!(worktree.entry_for_path("README.rst").unwrap().id, entry.id)
1182    });
1183}
1184
1185#[gpui::test]
1186async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1187    let text_2 = "
1188        fn one() -> usize {
1189            1
1190        }
1191    "
1192    .unindent();
1193    let text_1 = "
1194        fn one() -> usize {
1195            0
1196        }
1197    "
1198    .unindent();
1199
1200    let fs = FakeFs::new(server_cx.executor());
1201    fs.insert_tree(
1202        "/code",
1203        json!({
1204            "project1": {
1205                ".git": {},
1206                "src": {
1207                    "lib.rs": text_2
1208                },
1209                "README.md": "# project 1",
1210            },
1211        }),
1212    )
1213    .await;
1214    fs.set_index_for_repo(
1215        Path::new("/code/project1/.git"),
1216        &[("src/lib.rs".into(), text_1.clone())],
1217    );
1218    fs.set_head_for_repo(
1219        Path::new("/code/project1/.git"),
1220        &[("src/lib.rs".into(), text_1.clone())],
1221    );
1222
1223    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1224    let (worktree, _) = project
1225        .update(cx, |project, cx| {
1226            project.find_or_create_worktree("/code/project1", true, cx)
1227        })
1228        .await
1229        .unwrap();
1230    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1231    cx.executor().run_until_parked();
1232
1233    let buffer = project
1234        .update(cx, |project, cx| {
1235            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1236        })
1237        .await
1238        .unwrap();
1239    let diff = project
1240        .update(cx, |project, cx| {
1241            project.open_uncommitted_diff(buffer.clone(), cx)
1242        })
1243        .await
1244        .unwrap();
1245
1246    diff.read_with(cx, |diff, cx| {
1247        assert_eq!(diff.base_text_string().unwrap(), text_1);
1248        assert_eq!(
1249            diff.secondary_diff()
1250                .unwrap()
1251                .read(cx)
1252                .base_text_string()
1253                .unwrap(),
1254            text_1
1255        );
1256    });
1257
1258    // stage the current buffer's contents
1259    fs.set_index_for_repo(
1260        Path::new("/code/project1/.git"),
1261        &[("src/lib.rs".into(), text_2.clone())],
1262    );
1263
1264    cx.executor().run_until_parked();
1265    diff.read_with(cx, |diff, cx| {
1266        assert_eq!(diff.base_text_string().unwrap(), text_1);
1267        assert_eq!(
1268            diff.secondary_diff()
1269                .unwrap()
1270                .read(cx)
1271                .base_text_string()
1272                .unwrap(),
1273            text_2
1274        );
1275    });
1276
1277    // commit the current buffer's contents
1278    fs.set_head_for_repo(
1279        Path::new("/code/project1/.git"),
1280        &[("src/lib.rs".into(), text_2.clone())],
1281    );
1282
1283    cx.executor().run_until_parked();
1284    diff.read_with(cx, |diff, cx| {
1285        assert_eq!(diff.base_text_string().unwrap(), text_2);
1286        assert_eq!(
1287            diff.secondary_diff()
1288                .unwrap()
1289                .read(cx)
1290                .base_text_string()
1291                .unwrap(),
1292            text_2
1293        );
1294    });
1295}
1296
1297#[gpui::test]
1298async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1299    let fs = FakeFs::new(server_cx.executor());
1300    fs.insert_tree(
1301        "/code",
1302        json!({
1303            "project1": {
1304                ".git": {},
1305                "README.md": "# project 1",
1306            },
1307        }),
1308    )
1309    .await;
1310
1311    let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1312    let branches = ["main", "dev", "feature-1"];
1313    let branches_set = branches
1314        .iter()
1315        .map(ToString::to_string)
1316        .collect::<HashSet<_>>();
1317    fs.insert_branches(Path::new("/code/project1/.git"), &branches);
1318
1319    let (worktree, _) = project
1320        .update(cx, |project, cx| {
1321            project.find_or_create_worktree("/code/project1", true, cx)
1322        })
1323        .await
1324        .unwrap();
1325
1326    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1327    let root_path = ProjectPath::root_path(worktree_id);
1328    // Give the worktree a bit of time to index the file system
1329    cx.run_until_parked();
1330
1331    let remote_branches = project
1332        .update(cx, |project, cx| project.branches(root_path.clone(), cx))
1333        .await
1334        .unwrap();
1335
1336    let new_branch = branches[2];
1337
1338    let remote_branches = remote_branches
1339        .into_iter()
1340        .map(|branch| branch.name.to_string())
1341        .collect::<HashSet<_>>();
1342
1343    assert_eq!(&remote_branches, &branches_set);
1344
1345    cx.update(|cx| {
1346        project.update(cx, |project, cx| {
1347            project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
1348        })
1349    })
1350    .await
1351    .unwrap();
1352
1353    cx.run_until_parked();
1354
1355    let server_branch = server_cx.update(|cx| {
1356        headless_project.update(cx, |headless_project, cx| {
1357            headless_project
1358                .worktree_store
1359                .update(cx, |worktree_store, cx| {
1360                    worktree_store
1361                        .current_branch(root_path.clone(), cx)
1362                        .unwrap()
1363                })
1364        })
1365    });
1366
1367    assert_eq!(server_branch.name, branches[2]);
1368
1369    // Also try creating a new branch
1370    cx.update(|cx| {
1371        project.update(cx, |project, cx| {
1372            project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
1373        })
1374    })
1375    .await
1376    .unwrap();
1377
1378    cx.run_until_parked();
1379
1380    let server_branch = server_cx.update(|cx| {
1381        headless_project.update(cx, |headless_project, cx| {
1382            headless_project
1383                .worktree_store
1384                .update(cx, |worktree_store, cx| {
1385                    worktree_store.current_branch(root_path, cx).unwrap()
1386                })
1387        })
1388    });
1389
1390    assert_eq!(server_branch.name, "totally-new-branch");
1391}
1392
1393pub async fn init_test(
1394    server_fs: &Arc<FakeFs>,
1395    cx: &mut TestAppContext,
1396    server_cx: &mut TestAppContext,
1397) -> (Entity<Project>, Entity<HeadlessProject>) {
1398    let server_fs = server_fs.clone();
1399    cx.update(|cx| {
1400        release_channel::init(SemanticVersion::default(), cx);
1401    });
1402    server_cx.update(|cx| {
1403        release_channel::init(SemanticVersion::default(), cx);
1404    });
1405    init_logger();
1406
1407    let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
1408    let http_client = Arc::new(BlockedHttpClient);
1409    let node_runtime = NodeRuntime::unavailable();
1410    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1411    let proxy = Arc::new(ExtensionHostProxy::new());
1412    server_cx.update(HeadlessProject::init);
1413    let headless = server_cx.new(|cx| {
1414        client::init_settings(cx);
1415
1416        HeadlessProject::new(
1417            crate::HeadlessAppState {
1418                session: ssh_server_client,
1419                fs: server_fs.clone(),
1420                http_client,
1421                node_runtime,
1422                languages,
1423                extension_host_proxy: proxy,
1424            },
1425            cx,
1426        )
1427    });
1428
1429    let ssh = SshRemoteClient::fake_client(opts, cx).await;
1430    let project = build_project(ssh, cx);
1431    project
1432        .update(cx, {
1433            let headless = headless.clone();
1434            |_, cx| cx.on_release(|_, _| drop(headless))
1435        })
1436        .detach();
1437    (project, headless)
1438}
1439
1440fn init_logger() {
1441    if std::env::var("RUST_LOG").is_ok() {
1442        env_logger::try_init().ok();
1443    }
1444}
1445
1446fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1447    cx.update(|cx| {
1448        if !cx.has_global::<SettingsStore>() {
1449            let settings_store = SettingsStore::test(cx);
1450            cx.set_global(settings_store);
1451        }
1452    });
1453
1454    let client = cx.update(|cx| {
1455        Client::new(
1456            Arc::new(FakeSystemClock::new()),
1457            FakeHttpClient::with_404_response(),
1458            cx,
1459        )
1460    });
1461
1462    let node = NodeRuntime::unavailable();
1463    let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1464    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1465    let fs = FakeFs::new(cx.executor());
1466
1467    cx.update(|cx| {
1468        Project::init(&client, cx);
1469        language::init(cx);
1470    });
1471
1472    cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1473}