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