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