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