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