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 dap::DapRegistry;
   8use extension::ExtensionHostProxy;
   9use fs::{FakeFs, Fs};
  10use gpui::{AppContext as _, Entity, SemanticVersion, TestAppContext};
  11use http_client::{BlockedHttpClient, FakeHttpClient};
  12use language::{
  13    Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding,
  14    language_settings::{AllLanguageSettings, language_settings},
  15};
  16use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName};
  17use node_runtime::NodeRuntime;
  18use project::{
  19    Project, ProjectPath,
  20    search::{SearchQuery, SearchResult},
  21};
  22use remote::SshRemoteClient;
  23use serde_json::json;
  24use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};
  25use smol::stream::StreamExt;
  26use std::{
  27    collections::HashSet,
  28    path::{Path, PathBuf},
  29    sync::Arc,
  30};
  31#[cfg(not(windows))]
  32use unindent::Unindent as _;
  33use util::{path, separator};
  34
  35#[gpui::test]
  36async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
  37    let fs = FakeFs::new(server_cx.executor());
  38    fs.insert_tree(
  39        path!("/code"),
  40        json!({
  41            "project1": {
  42                ".git": {},
  43                "README.md": "# project 1",
  44                "src": {
  45                    "lib.rs": "fn one() -> usize { 1 }"
  46                }
  47            },
  48            "project2": {
  49                "README.md": "# project 2",
  50            },
  51        }),
  52    )
  53    .await;
  54    fs.set_index_for_repo(
  55        Path::new(path!("/code/project1/.git")),
  56        &[("src/lib.rs".into(), "fn one() -> usize { 0 }".into())],
  57    );
  58
  59    let (project, _headless) = init_test(&fs, cx, server_cx).await;
  60    let (worktree, _) = project
  61        .update(cx, |project, cx| {
  62            project.find_or_create_worktree(path!("/code/project1"), true, cx)
  63        })
  64        .await
  65        .unwrap();
  66
  67    // The client sees the worktree's contents.
  68    cx.executor().run_until_parked();
  69    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
  70    worktree.update(cx, |worktree, _cx| {
  71        assert_eq!(
  72            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
  73            vec![
  74                Path::new("README.md"),
  75                Path::new("src"),
  76                Path::new("src/lib.rs"),
  77            ]
  78        );
  79    });
  80
  81    // The user opens a buffer in the remote worktree. The buffer's
  82    // contents are loaded from the remote filesystem.
  83    let buffer = project
  84        .update(cx, |project, cx| {
  85            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
  86        })
  87        .await
  88        .unwrap();
  89    let diff = project
  90        .update(cx, |project, cx| {
  91            project.open_unstaged_diff(buffer.clone(), cx)
  92        })
  93        .await
  94        .unwrap();
  95
  96    diff.update(cx, |diff, _| {
  97        assert_eq!(diff.base_text_string().unwrap(), "fn one() -> usize { 0 }");
  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    diff.update(cx, |diff, _| {
 158        assert_eq!(
 159            diff.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.set_request_handler::<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
 512            .unwrap()
 513            .into_iter()
 514            .map(|c| c.label.text)
 515            .collect::<Vec<_>>(),
 516        vec!["boop".to_string()]
 517    );
 518
 519    fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {
 520        Ok(Some(lsp::WorkspaceEdit {
 521            changes: Some(
 522                [(
 523                    lsp::Url::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
 524                    vec![lsp::TextEdit::new(
 525                        lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
 526                        "two".to_string(),
 527                    )],
 528                )]
 529                .into_iter()
 530                .collect(),
 531            ),
 532            ..Default::default()
 533        }))
 534    });
 535
 536    project
 537        .update(cx, |project, cx| {
 538            project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
 539        })
 540        .await
 541        .unwrap();
 542
 543    cx.run_until_parked();
 544    buffer.update(cx, |buffer, _| {
 545        assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
 546    })
 547}
 548
 549#[gpui::test]
 550async fn test_remote_cancel_language_server_work(
 551    cx: &mut TestAppContext,
 552    server_cx: &mut TestAppContext,
 553) {
 554    let fs = FakeFs::new(server_cx.executor());
 555    fs.insert_tree(
 556        path!("/code"),
 557        json!({
 558            "project1": {
 559                ".git": {},
 560                "README.md": "# project 1",
 561                "src": {
 562                    "lib.rs": "fn one() -> usize { 1 }"
 563                }
 564            },
 565        }),
 566    )
 567    .await;
 568
 569    let (project, headless) = init_test(&fs, cx, server_cx).await;
 570
 571    fs.insert_tree(
 572        path!("/code/project1/.zed"),
 573        json!({
 574            "settings.json": r#"
 575          {
 576            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
 577            "lsp": {
 578              "rust-analyzer": {
 579                "binary": {
 580                  "path": "~/.cargo/bin/rust-analyzer"
 581                }
 582              }
 583            }
 584          }"#
 585        }),
 586    )
 587    .await;
 588
 589    cx.update_entity(&project, |project, _| {
 590        project.languages().register_test_language(LanguageConfig {
 591            name: "Rust".into(),
 592            matcher: LanguageMatcher {
 593                path_suffixes: vec!["rs".into()],
 594                ..Default::default()
 595            },
 596            ..Default::default()
 597        });
 598        project.languages().register_fake_lsp_adapter(
 599            "Rust",
 600            FakeLspAdapter {
 601                name: "rust-analyzer",
 602                ..Default::default()
 603            },
 604        )
 605    });
 606
 607    let mut fake_lsp = server_cx.update(|cx| {
 608        headless.read(cx).languages.register_fake_language_server(
 609            LanguageServerName("rust-analyzer".into()),
 610            Default::default(),
 611            None,
 612        )
 613    });
 614
 615    cx.run_until_parked();
 616
 617    let worktree_id = project
 618        .update(cx, |project, cx| {
 619            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 620        })
 621        .await
 622        .unwrap()
 623        .0
 624        .read_with(cx, |worktree, _| worktree.id());
 625
 626    cx.run_until_parked();
 627
 628    let (buffer, _handle) = project
 629        .update(cx, |project, cx| {
 630            project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
 631        })
 632        .await
 633        .unwrap();
 634
 635    cx.run_until_parked();
 636
 637    let mut fake_lsp = fake_lsp.next().await.unwrap();
 638
 639    // Cancelling all language server work for a given buffer
 640    {
 641        // Two operations, one cancellable and one not.
 642        fake_lsp
 643            .start_progress_with(
 644                "another-token",
 645                lsp::WorkDoneProgressBegin {
 646                    cancellable: Some(false),
 647                    ..Default::default()
 648                },
 649            )
 650            .await;
 651
 652        let progress_token = "the-progress-token";
 653        fake_lsp
 654            .start_progress_with(
 655                progress_token,
 656                lsp::WorkDoneProgressBegin {
 657                    cancellable: Some(true),
 658                    ..Default::default()
 659                },
 660            )
 661            .await;
 662
 663        cx.executor().run_until_parked();
 664
 665        project.update(cx, |project, cx| {
 666            project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
 667        });
 668
 669        cx.executor().run_until_parked();
 670
 671        // Verify the cancellation was received on the server side
 672        let cancel_notification = fake_lsp
 673            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
 674            .await;
 675        assert_eq!(
 676            cancel_notification.token,
 677            lsp::NumberOrString::String(progress_token.into())
 678        );
 679    }
 680
 681    // Cancelling work by server_id and token
 682    {
 683        let server_id = fake_lsp.server.server_id();
 684        let progress_token = "the-progress-token";
 685
 686        fake_lsp
 687            .start_progress_with(
 688                progress_token,
 689                lsp::WorkDoneProgressBegin {
 690                    cancellable: Some(true),
 691                    ..Default::default()
 692                },
 693            )
 694            .await;
 695
 696        cx.executor().run_until_parked();
 697
 698        project.update(cx, |project, cx| {
 699            project.cancel_language_server_work(server_id, Some(progress_token.into()), cx)
 700        });
 701
 702        cx.executor().run_until_parked();
 703
 704        // Verify the cancellation was received on the server side
 705        let cancel_notification = fake_lsp
 706            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
 707            .await;
 708        assert_eq!(
 709            cancel_notification.token,
 710            lsp::NumberOrString::String(progress_token.into())
 711        );
 712    }
 713}
 714
 715#[gpui::test]
 716async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 717    let fs = FakeFs::new(server_cx.executor());
 718    fs.insert_tree(
 719        path!("/code"),
 720        json!({
 721            "project1": {
 722                ".git": {},
 723                "README.md": "# project 1",
 724                "src": {
 725                    "lib.rs": "fn one() -> usize { 1 }"
 726                }
 727            },
 728        }),
 729    )
 730    .await;
 731
 732    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 733    let (worktree, _) = project
 734        .update(cx, |project, cx| {
 735            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 736        })
 737        .await
 738        .unwrap();
 739
 740    let worktree_id = cx.update(|cx| worktree.read(cx).id());
 741
 742    let buffer = project
 743        .update(cx, |project, cx| {
 744            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 745        })
 746        .await
 747        .unwrap();
 748
 749    fs.save(
 750        &PathBuf::from(path!("/code/project1/src/lib.rs")),
 751        &("bangles".to_string().into()),
 752        LineEnding::Unix,
 753    )
 754    .await
 755    .unwrap();
 756
 757    cx.run_until_parked();
 758
 759    buffer.update(cx, |buffer, cx| {
 760        assert_eq!(buffer.text(), "bangles");
 761        buffer.edit([(0..0, "a")], None, cx);
 762    });
 763
 764    fs.save(
 765        &PathBuf::from(path!("/code/project1/src/lib.rs")),
 766        &("bloop".to_string().into()),
 767        LineEnding::Unix,
 768    )
 769    .await
 770    .unwrap();
 771
 772    cx.run_until_parked();
 773    cx.update(|cx| {
 774        assert!(buffer.read(cx).has_conflict());
 775    });
 776
 777    project
 778        .update(cx, |project, cx| {
 779            project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
 780        })
 781        .await
 782        .unwrap();
 783    cx.run_until_parked();
 784
 785    cx.update(|cx| {
 786        assert!(!buffer.read(cx).has_conflict());
 787    });
 788}
 789
 790#[gpui::test]
 791async fn test_remote_resolve_path_in_buffer(
 792    cx: &mut TestAppContext,
 793    server_cx: &mut TestAppContext,
 794) {
 795    let fs = FakeFs::new(server_cx.executor());
 796    // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
 797    fs.insert_tree(
 798        path!("/code"),
 799        json!({
 800            "project1": {
 801                ".git": {},
 802                "README.md": "# project 1",
 803                "src": {
 804                    "lib.rs": "fn one() -> usize { 1 }"
 805                }
 806            },
 807            "project2": {
 808                ".git": {},
 809                "README.md": "# project 2",
 810                "src": {
 811                    "lib.rs": "fn two() -> usize { 2 }"
 812                }
 813            }
 814        }),
 815    )
 816    .await;
 817
 818    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 819
 820    let _ = project
 821        .update(cx, |project, cx| {
 822            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 823        })
 824        .await
 825        .unwrap();
 826
 827    let (worktree2, _) = project
 828        .update(cx, |project, cx| {
 829            project.find_or_create_worktree(path!("/code/project2"), true, cx)
 830        })
 831        .await
 832        .unwrap();
 833
 834    let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
 835
 836    let buffer2 = project
 837        .update(cx, |project, cx| {
 838            project.open_buffer((worktree2_id, Path::new("src/lib.rs")), cx)
 839        })
 840        .await
 841        .unwrap();
 842
 843    let path = project
 844        .update(cx, |project, cx| {
 845            project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
 846        })
 847        .await
 848        .unwrap();
 849    assert!(path.is_file());
 850    assert_eq!(
 851        path.abs_path().unwrap().to_string_lossy(),
 852        path!("/code/project2/README.md")
 853    );
 854
 855    let path = project
 856        .update(cx, |project, cx| {
 857            project.resolve_path_in_buffer("../README.md", &buffer2, cx)
 858        })
 859        .await
 860        .unwrap();
 861    assert!(path.is_file());
 862    assert_eq!(
 863        path.project_path().unwrap().clone(),
 864        ProjectPath::from((worktree2_id, "README.md"))
 865    );
 866
 867    let path = project
 868        .update(cx, |project, cx| {
 869            project.resolve_path_in_buffer("../src", &buffer2, cx)
 870        })
 871        .await
 872        .unwrap();
 873    assert_eq!(
 874        path.project_path().unwrap().clone(),
 875        ProjectPath::from((worktree2_id, "src"))
 876    );
 877    assert!(path.is_dir());
 878}
 879
 880#[gpui::test]
 881async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 882    let fs = FakeFs::new(server_cx.executor());
 883    fs.insert_tree(
 884        path!("/code"),
 885        json!({
 886            "project1": {
 887                ".git": {},
 888                "README.md": "# project 1",
 889                "src": {
 890                    "lib.rs": "fn one() -> usize { 1 }"
 891                }
 892            },
 893        }),
 894    )
 895    .await;
 896
 897    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 898
 899    let path = project
 900        .update(cx, |project, cx| {
 901            project.resolve_abs_path(path!("/code/project1/README.md"), cx)
 902        })
 903        .await
 904        .unwrap();
 905
 906    assert!(path.is_file());
 907    assert_eq!(
 908        path.abs_path().unwrap().to_string_lossy(),
 909        path!("/code/project1/README.md")
 910    );
 911
 912    let path = project
 913        .update(cx, |project, cx| {
 914            project.resolve_abs_path(path!("/code/project1/src"), cx)
 915        })
 916        .await
 917        .unwrap();
 918
 919    assert!(path.is_dir());
 920    assert_eq!(
 921        path.abs_path().unwrap().to_string_lossy(),
 922        path!("/code/project1/src")
 923    );
 924
 925    let path = project
 926        .update(cx, |project, cx| {
 927            project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
 928        })
 929        .await;
 930    assert!(path.is_none());
 931}
 932
 933#[gpui::test(iterations = 10)]
 934async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 935    let fs = FakeFs::new(server_cx.executor());
 936    fs.insert_tree(
 937        "/code",
 938        json!({
 939            "project1": {
 940                ".git": {},
 941                "README.md": "# project 1",
 942                "src": {
 943                    "lib.rs": "fn one() -> usize { 1 }"
 944                }
 945            },
 946        }),
 947    )
 948    .await;
 949
 950    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 951    let (worktree, _) = project
 952        .update(cx, |project, cx| {
 953            project.find_or_create_worktree("/code/project1", true, cx)
 954        })
 955        .await
 956        .unwrap();
 957    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 958
 959    // Open a buffer on the client but cancel after a random amount of time.
 960    let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
 961    cx.executor().simulate_random_delay().await;
 962    drop(buffer);
 963
 964    // Try opening the same buffer again as the client, and ensure we can
 965    // still do it despite the cancellation above.
 966    let buffer = project
 967        .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
 968        .await
 969        .unwrap();
 970
 971    buffer.read_with(cx, |buf, _| {
 972        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
 973    });
 974}
 975
 976#[gpui::test]
 977async fn test_adding_then_removing_then_adding_worktrees(
 978    cx: &mut TestAppContext,
 979    server_cx: &mut TestAppContext,
 980) {
 981    let fs = FakeFs::new(server_cx.executor());
 982    fs.insert_tree(
 983        path!("/code"),
 984        json!({
 985            "project1": {
 986                ".git": {},
 987                "README.md": "# project 1",
 988                "src": {
 989                    "lib.rs": "fn one() -> usize { 1 }"
 990                }
 991            },
 992            "project2": {
 993                "README.md": "# project 2",
 994            },
 995        }),
 996    )
 997    .await;
 998
 999    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1000    let (_worktree, _) = project
1001        .update(cx, |project, cx| {
1002            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1003        })
1004        .await
1005        .unwrap();
1006
1007    let (worktree_2, _) = project
1008        .update(cx, |project, cx| {
1009            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1010        })
1011        .await
1012        .unwrap();
1013    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1014
1015    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1016
1017    let (worktree_2, _) = project
1018        .update(cx, |project, cx| {
1019            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1020        })
1021        .await
1022        .unwrap();
1023
1024    cx.run_until_parked();
1025    worktree_2.update(cx, |worktree, _cx| {
1026        assert!(worktree.is_visible());
1027        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1028        assert_eq!(entries.len(), 2);
1029        assert_eq!(
1030            entries[1].path.to_string_lossy().to_string(),
1031            "README.md".to_string()
1032        )
1033    })
1034}
1035
1036#[gpui::test]
1037async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1038    let fs = FakeFs::new(server_cx.executor());
1039    fs.insert_tree(
1040        path!("/code"),
1041        json!({
1042            "project1": {
1043                ".git": {},
1044                "README.md": "# project 1",
1045                "src": {
1046                    "lib.rs": "fn one() -> usize { 1 }"
1047                }
1048            },
1049        }),
1050    )
1051    .await;
1052
1053    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1054    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1055    cx.executor().run_until_parked();
1056
1057    let buffer = buffer.await.unwrap();
1058
1059    cx.update(|cx| {
1060        assert_eq!(
1061            buffer.read(cx).text(),
1062            initial_server_settings_content()
1063                .to_string()
1064                .replace("\r\n", "\n")
1065        )
1066    })
1067}
1068
1069#[gpui::test(iterations = 20)]
1070async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1071    let fs = FakeFs::new(server_cx.executor());
1072    fs.insert_tree(
1073        path!("/code"),
1074        json!({
1075            "project1": {
1076                ".git": {},
1077                "README.md": "# project 1",
1078                "src": {
1079                    "lib.rs": "fn one() -> usize { 1 }"
1080                }
1081            },
1082        }),
1083    )
1084    .await;
1085
1086    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1087
1088    let (worktree, _) = project
1089        .update(cx, |project, cx| {
1090            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1091        })
1092        .await
1093        .unwrap();
1094
1095    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1096    let buffer = project
1097        .update(cx, |project, cx| {
1098            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1099        })
1100        .await
1101        .unwrap();
1102
1103    buffer.update(cx, |buffer, cx| {
1104        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1105        let ix = buffer.text().find('1').unwrap();
1106        buffer.edit([(ix..ix + 1, "100")], None, cx);
1107    });
1108
1109    let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
1110    client
1111        .update(cx, |client, cx| client.simulate_disconnect(cx))
1112        .detach();
1113
1114    project
1115        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1116        .await
1117        .unwrap();
1118
1119    assert_eq!(
1120        fs.load(path!("/code/project1/src/lib.rs").as_ref())
1121            .await
1122            .unwrap(),
1123        "fn one() -> usize { 100 }"
1124    );
1125}
1126
1127#[gpui::test]
1128async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1129    let fs = FakeFs::new(server_cx.executor());
1130    fs.insert_tree(
1131        "/code",
1132        json!({
1133            "project1": {
1134                ".git": {},
1135                "README.md": "# project 1",
1136            },
1137        }),
1138    )
1139    .await;
1140
1141    let (project, _) = init_test(&fs, cx, server_cx).await;
1142
1143    let (worktree, _) = project
1144        .update(cx, |project, cx| {
1145            project.find_or_create_worktree("/code/project1", true, cx)
1146        })
1147        .await
1148        .unwrap();
1149
1150    cx.run_until_parked();
1151
1152    fs.rename(
1153        &PathBuf::from("/code/project1"),
1154        &PathBuf::from("/code/project2"),
1155        Default::default(),
1156    )
1157    .await
1158    .unwrap();
1159
1160    cx.run_until_parked();
1161    worktree.update(cx, |worktree, _| {
1162        assert_eq!(worktree.root_name(), "project2")
1163    })
1164}
1165
1166#[gpui::test]
1167async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1168    let fs = FakeFs::new(server_cx.executor());
1169    fs.insert_tree(
1170        "/code",
1171        json!({
1172            "project1": {
1173                ".git": {},
1174                "README.md": "# project 1",
1175            },
1176        }),
1177    )
1178    .await;
1179
1180    let (project, _) = init_test(&fs, cx, server_cx).await;
1181    let (worktree, _) = project
1182        .update(cx, |project, cx| {
1183            project.find_or_create_worktree("/code/project1", true, cx)
1184        })
1185        .await
1186        .unwrap();
1187
1188    cx.run_until_parked();
1189
1190    let entry = worktree
1191        .update(cx, |worktree, cx| {
1192            let entry = worktree.entry_for_path("README.md").unwrap();
1193            worktree.rename_entry(entry.id, Path::new("README.rst"), cx)
1194        })
1195        .await
1196        .unwrap()
1197        .to_included()
1198        .unwrap();
1199
1200    cx.run_until_parked();
1201
1202    worktree.update(cx, |worktree, _| {
1203        assert_eq!(worktree.entry_for_path("README.rst").unwrap().id, entry.id)
1204    });
1205}
1206
1207// TODO: this test fails on Windows.
1208#[cfg(not(windows))]
1209#[gpui::test]
1210async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1211    let text_2 = "
1212        fn one() -> usize {
1213            1
1214        }
1215    "
1216    .unindent();
1217    let text_1 = "
1218        fn one() -> usize {
1219            0
1220        }
1221    "
1222    .unindent();
1223
1224    let fs = FakeFs::new(server_cx.executor());
1225    fs.insert_tree(
1226        "/code",
1227        json!({
1228            "project1": {
1229                ".git": {},
1230                "src": {
1231                    "lib.rs": text_2
1232                },
1233                "README.md": "# project 1",
1234            },
1235        }),
1236    )
1237    .await;
1238    fs.set_index_for_repo(
1239        Path::new("/code/project1/.git"),
1240        &[("src/lib.rs".into(), text_1.clone())],
1241    );
1242    fs.set_head_for_repo(
1243        Path::new("/code/project1/.git"),
1244        &[("src/lib.rs".into(), text_1.clone())],
1245    );
1246
1247    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1248    let (worktree, _) = project
1249        .update(cx, |project, cx| {
1250            project.find_or_create_worktree("/code/project1", true, cx)
1251        })
1252        .await
1253        .unwrap();
1254    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1255    cx.executor().run_until_parked();
1256
1257    let buffer = project
1258        .update(cx, |project, cx| {
1259            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1260        })
1261        .await
1262        .unwrap();
1263    let diff = project
1264        .update(cx, |project, cx| {
1265            project.open_uncommitted_diff(buffer.clone(), cx)
1266        })
1267        .await
1268        .unwrap();
1269
1270    diff.read_with(cx, |diff, cx| {
1271        assert_eq!(diff.base_text_string().unwrap(), text_1);
1272        assert_eq!(
1273            diff.secondary_diff()
1274                .unwrap()
1275                .read(cx)
1276                .base_text_string()
1277                .unwrap(),
1278            text_1
1279        );
1280    });
1281
1282    // stage the current buffer's contents
1283    fs.set_index_for_repo(
1284        Path::new("/code/project1/.git"),
1285        &[("src/lib.rs".into(), text_2.clone())],
1286    );
1287
1288    cx.executor().run_until_parked();
1289    diff.read_with(cx, |diff, cx| {
1290        assert_eq!(diff.base_text_string().unwrap(), text_1);
1291        assert_eq!(
1292            diff.secondary_diff()
1293                .unwrap()
1294                .read(cx)
1295                .base_text_string()
1296                .unwrap(),
1297            text_2
1298        );
1299    });
1300
1301    // commit the current buffer's contents
1302    fs.set_head_for_repo(
1303        Path::new("/code/project1/.git"),
1304        &[("src/lib.rs".into(), text_2.clone())],
1305    );
1306
1307    cx.executor().run_until_parked();
1308    diff.read_with(cx, |diff, cx| {
1309        assert_eq!(diff.base_text_string().unwrap(), text_2);
1310        assert_eq!(
1311            diff.secondary_diff()
1312                .unwrap()
1313                .read(cx)
1314                .base_text_string()
1315                .unwrap(),
1316            text_2
1317        );
1318    });
1319}
1320
1321#[gpui::test]
1322async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1323    let fs = FakeFs::new(server_cx.executor());
1324    fs.insert_tree(
1325        path!("/code"),
1326        json!({
1327            "project1": {
1328                ".git": {},
1329                "README.md": "# project 1",
1330            },
1331        }),
1332    )
1333    .await;
1334
1335    let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1336    let branches = ["main", "dev", "feature-1"];
1337    let branches_set = branches
1338        .iter()
1339        .map(ToString::to_string)
1340        .collect::<HashSet<_>>();
1341    fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1342
1343    let (_worktree, _) = project
1344        .update(cx, |project, cx| {
1345            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1346        })
1347        .await
1348        .unwrap();
1349    // Give the worktree a bit of time to index the file system
1350    cx.run_until_parked();
1351
1352    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1353
1354    let remote_branches = repository
1355        .update(cx, |repository, _| repository.branches())
1356        .await
1357        .unwrap()
1358        .unwrap();
1359
1360    let new_branch = branches[2];
1361
1362    let remote_branches = remote_branches
1363        .into_iter()
1364        .map(|branch| branch.name.to_string())
1365        .collect::<HashSet<_>>();
1366
1367    assert_eq!(&remote_branches, &branches_set);
1368
1369    cx.update(|cx| {
1370        repository.update(cx, |repository, _cx| {
1371            repository.change_branch(new_branch.to_string())
1372        })
1373    })
1374    .await
1375    .unwrap()
1376    .unwrap();
1377
1378    cx.run_until_parked();
1379
1380    let server_branch = server_cx.update(|cx| {
1381        headless_project.update(cx, |headless_project, cx| {
1382            headless_project.git_store.update(cx, |git_store, cx| {
1383                git_store
1384                    .repositories()
1385                    .values()
1386                    .next()
1387                    .unwrap()
1388                    .read(cx)
1389                    .branch
1390                    .as_ref()
1391                    .unwrap()
1392                    .clone()
1393            })
1394        })
1395    });
1396
1397    assert_eq!(server_branch.name, branches[2]);
1398
1399    // Also try creating a new branch
1400    cx.update(|cx| {
1401        repository.update(cx, |repo, _cx| {
1402            repo.create_branch("totally-new-branch".to_string())
1403        })
1404    })
1405    .await
1406    .unwrap()
1407    .unwrap();
1408
1409    cx.update(|cx| {
1410        repository.update(cx, |repo, _cx| {
1411            repo.change_branch("totally-new-branch".to_string())
1412        })
1413    })
1414    .await
1415    .unwrap()
1416    .unwrap();
1417
1418    cx.run_until_parked();
1419
1420    let server_branch = server_cx.update(|cx| {
1421        headless_project.update(cx, |headless_project, cx| {
1422            headless_project.git_store.update(cx, |git_store, cx| {
1423                git_store
1424                    .repositories()
1425                    .values()
1426                    .next()
1427                    .unwrap()
1428                    .read(cx)
1429                    .branch
1430                    .as_ref()
1431                    .unwrap()
1432                    .clone()
1433            })
1434        })
1435    });
1436
1437    assert_eq!(server_branch.name, "totally-new-branch");
1438}
1439
1440pub async fn init_test(
1441    server_fs: &Arc<FakeFs>,
1442    cx: &mut TestAppContext,
1443    server_cx: &mut TestAppContext,
1444) -> (Entity<Project>, Entity<HeadlessProject>) {
1445    let server_fs = server_fs.clone();
1446    cx.update(|cx| {
1447        release_channel::init(SemanticVersion::default(), cx);
1448    });
1449    server_cx.update(|cx| {
1450        release_channel::init(SemanticVersion::default(), cx);
1451    });
1452    init_logger();
1453
1454    let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
1455    let http_client = Arc::new(BlockedHttpClient);
1456    let node_runtime = NodeRuntime::unavailable();
1457    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1458    let debug_adapters = DapRegistry::default().into();
1459    let proxy = Arc::new(ExtensionHostProxy::new());
1460    server_cx.update(HeadlessProject::init);
1461    let headless = server_cx.new(|cx| {
1462        client::init_settings(cx);
1463
1464        HeadlessProject::new(
1465            crate::HeadlessAppState {
1466                session: ssh_server_client,
1467                fs: server_fs.clone(),
1468                http_client,
1469                node_runtime,
1470                languages,
1471                debug_adapters,
1472                extension_host_proxy: proxy,
1473            },
1474            cx,
1475        )
1476    });
1477
1478    let ssh = SshRemoteClient::fake_client(opts, cx).await;
1479    let project = build_project(ssh, cx);
1480    project
1481        .update(cx, {
1482            let headless = headless.clone();
1483            |_, cx| cx.on_release(|_, _| drop(headless))
1484        })
1485        .detach();
1486    (project, headless)
1487}
1488
1489fn init_logger() {
1490    if std::env::var("RUST_LOG").is_ok() {
1491        env_logger::try_init().ok();
1492    }
1493}
1494
1495fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1496    cx.update(|cx| {
1497        if !cx.has_global::<SettingsStore>() {
1498            let settings_store = SettingsStore::test(cx);
1499            cx.set_global(settings_store);
1500        }
1501    });
1502
1503    let client = cx.update(|cx| {
1504        Client::new(
1505            Arc::new(FakeSystemClock::new()),
1506            FakeHttpClient::with_404_response(),
1507            cx,
1508        )
1509    });
1510
1511    let node = NodeRuntime::unavailable();
1512    let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1513    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1514    let fs = FakeFs::new(cx.executor());
1515
1516    cx.update(|cx| {
1517        Project::init(&client, cx);
1518        language::init(cx);
1519    });
1520
1521    cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1522}