remote_editing_tests.rs

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