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 agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream, ToolInput};
   6use client::{Client, UserStore};
   7use clock::FakeSystemClock;
   8use collections::{HashMap, HashSet};
   9use language_model::LanguageModelToolResultContent;
  10use languages::rust_lang;
  11
  12use extension::ExtensionHostProxy;
  13use fs::{FakeFs, Fs};
  14use git::repository::Worktree as GitWorktree;
  15use gpui::{AppContext as _, Entity, SharedString, TestAppContext};
  16use http_client::{BlockedHttpClient, FakeHttpClient};
  17use language::{
  18    Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding,
  19    language_settings::{AllLanguageSettings, LanguageSettings},
  20};
  21use lsp::{
  22    CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT,
  23    LanguageServerName,
  24};
  25use node_runtime::NodeRuntime;
  26use project::{
  27    ProgressToken, Project,
  28    agent_server_store::AgentServerCommand,
  29    search::{SearchQuery, SearchResult},
  30};
  31use remote::RemoteClient;
  32use serde_json::json;
  33use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};
  34use smol::stream::StreamExt;
  35use std::{
  36    path::{Path, PathBuf},
  37    sync::Arc,
  38};
  39use unindent::Unindent as _;
  40use util::{path, paths::PathMatcher, rel_path::rel_path};
  41
  42#[gpui::test]
  43async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
  44    let fs = FakeFs::new(server_cx.executor());
  45    fs.insert_tree(
  46        path!("/code"),
  47        json!({
  48            "project1": {
  49                ".git": {},
  50                "README.md": "# project 1",
  51                "src": {
  52                    "lib.rs": "fn one() -> usize { 1 }"
  53                }
  54            },
  55            "project2": {
  56                "README.md": "# project 2",
  57            },
  58        }),
  59    )
  60    .await;
  61    fs.set_index_for_repo(
  62        Path::new(path!("/code/project1/.git")),
  63        &[("src/lib.rs", "fn one() -> usize { 0 }".into())],
  64    );
  65
  66    let (project, _headless) = init_test(&fs, cx, server_cx).await;
  67    let (worktree, _) = project
  68        .update(cx, |project, cx| {
  69            project.find_or_create_worktree(path!("/code/project1"), true, cx)
  70        })
  71        .await
  72        .unwrap();
  73
  74    // The client sees the worktree's contents.
  75    cx.executor().run_until_parked();
  76    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
  77    worktree.update(cx, |worktree, _cx| {
  78        assert_eq!(
  79            worktree.paths().collect::<Vec<_>>(),
  80            vec![
  81                rel_path("README.md"),
  82                rel_path("src"),
  83                rel_path("src/lib.rs"),
  84            ]
  85        );
  86    });
  87
  88    // The user opens a buffer in the remote worktree. The buffer's
  89    // contents are loaded from the remote filesystem.
  90    let buffer = project
  91        .update(cx, |project, cx| {
  92            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
  93        })
  94        .await
  95        .unwrap();
  96    let diff = project
  97        .update(cx, |project, cx| {
  98            project.open_unstaged_diff(buffer.clone(), cx)
  99        })
 100        .await
 101        .unwrap();
 102
 103    diff.update(cx, |diff, cx| {
 104        assert_eq!(
 105            diff.base_text_string(cx).unwrap(),
 106            "fn one() -> usize { 0 }"
 107        );
 108    });
 109
 110    buffer.update(cx, |buffer, cx| {
 111        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
 112        let ix = buffer.text().find('1').unwrap();
 113        buffer.edit([(ix..ix + 1, "100")], None, cx);
 114    });
 115
 116    // The user saves the buffer. The new contents are written to the
 117    // remote filesystem.
 118    project
 119        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 120        .await
 121        .unwrap();
 122    assert_eq!(
 123        fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
 124        "fn one() -> usize { 100 }"
 125    );
 126
 127    // A new file is created in the remote filesystem. The user
 128    // sees the new file.
 129    fs.save(
 130        path!("/code/project1/src/main.rs").as_ref(),
 131        &"fn main() {}".into(),
 132        Default::default(),
 133    )
 134    .await
 135    .unwrap();
 136    cx.executor().run_until_parked();
 137    worktree.update(cx, |worktree, _cx| {
 138        assert_eq!(
 139            worktree.paths().collect::<Vec<_>>(),
 140            vec![
 141                rel_path("README.md"),
 142                rel_path("src"),
 143                rel_path("src/lib.rs"),
 144                rel_path("src/main.rs"),
 145            ]
 146        );
 147    });
 148
 149    // A file that is currently open in a buffer is renamed.
 150    fs.rename(
 151        path!("/code/project1/src/lib.rs").as_ref(),
 152        path!("/code/project1/src/lib2.rs").as_ref(),
 153        Default::default(),
 154    )
 155    .await
 156    .unwrap();
 157    cx.executor().run_until_parked();
 158    buffer.update(cx, |buffer, _| {
 159        assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs"));
 160    });
 161
 162    fs.set_index_for_repo(
 163        Path::new(path!("/code/project1/.git")),
 164        &[("src/lib2.rs", "fn one() -> usize { 100 }".into())],
 165    );
 166    cx.executor().run_until_parked();
 167    diff.update(cx, |diff, cx| {
 168        assert_eq!(
 169            diff.base_text_string(cx).unwrap(),
 170            "fn one() -> usize { 100 }"
 171        );
 172    });
 173}
 174
 175async fn do_search_and_assert(
 176    project: &Entity<Project>,
 177    query: &str,
 178    files_to_include: PathMatcher,
 179    match_full_paths: bool,
 180    expected_paths: &[&str],
 181    mut cx: TestAppContext,
 182) -> Vec<Entity<Buffer>> {
 183    let query = query.to_string();
 184    let receiver = project.update(&mut cx, |project, cx| {
 185        project.search(
 186            SearchQuery::text(
 187                query,
 188                false,
 189                true,
 190                false,
 191                files_to_include,
 192                Default::default(),
 193                match_full_paths,
 194                None,
 195            )
 196            .unwrap(),
 197            cx,
 198        )
 199    });
 200
 201    let mut buffers = Vec::new();
 202    for expected_path in expected_paths {
 203        let response = receiver.rx.recv().await.unwrap();
 204        let SearchResult::Buffer { buffer, .. } = response else {
 205            panic!("incorrect result");
 206        };
 207        buffer.update(&mut cx, |buffer, cx| {
 208            assert_eq!(
 209                buffer.file().unwrap().full_path(cx).to_string_lossy(),
 210                *expected_path
 211            )
 212        });
 213        buffers.push(buffer);
 214    }
 215
 216    assert!(receiver.rx.recv().await.is_err());
 217    buffers
 218}
 219
 220#[gpui::test]
 221async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 222    let fs = FakeFs::new(server_cx.executor());
 223    fs.insert_tree(
 224        path!("/code"),
 225        json!({
 226            "project1": {
 227                ".git": {},
 228                "README.md": "# project 1",
 229                "src": {
 230                    "lib.rs": "fn one() -> usize { 1 }"
 231                }
 232            },
 233        }),
 234    )
 235    .await;
 236
 237    let (project, headless) = init_test(&fs, cx, server_cx).await;
 238
 239    project
 240        .update(cx, |project, cx| {
 241            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 242        })
 243        .await
 244        .unwrap();
 245
 246    cx.run_until_parked();
 247
 248    let buffers = do_search_and_assert(
 249        &project,
 250        "project",
 251        Default::default(),
 252        false,
 253        &[path!("project1/README.md")],
 254        cx.clone(),
 255    )
 256    .await;
 257    let buffer = buffers.into_iter().next().unwrap();
 258
 259    // test that the headless server is tracking which buffers we have open correctly.
 260    cx.run_until_parked();
 261    headless.update(server_cx, |headless, cx| {
 262        assert!(headless.buffer_store.read(cx).has_shared_buffers())
 263    });
 264    do_search_and_assert(
 265        &project,
 266        "project",
 267        Default::default(),
 268        false,
 269        &[path!("project1/README.md")],
 270        cx.clone(),
 271    )
 272    .await;
 273    server_cx.run_until_parked();
 274    cx.update(|_| {
 275        drop(buffer);
 276    });
 277    cx.run_until_parked();
 278    server_cx.run_until_parked();
 279    headless.update(server_cx, |headless, cx| {
 280        assert!(!headless.buffer_store.read(cx).has_shared_buffers())
 281    });
 282
 283    do_search_and_assert(
 284        &project,
 285        "project",
 286        Default::default(),
 287        false,
 288        &[path!("project1/README.md")],
 289        cx.clone(),
 290    )
 291    .await;
 292}
 293
 294#[gpui::test]
 295async fn test_remote_project_search_single_cpu(
 296    cx: &mut TestAppContext,
 297    server_cx: &mut TestAppContext,
 298) {
 299    let fs = FakeFs::new(server_cx.executor());
 300    fs.insert_tree(
 301        path!("/code"),
 302        json!({
 303            "project1": {
 304                ".git": {},
 305                "README.md": "# project 1",
 306                "src": {
 307                    "lib.rs": "fn one() -> usize { 1 }"
 308                }
 309            },
 310        }),
 311    )
 312    .await;
 313
 314    // Simulate a single-CPU environment (e.g. a devcontainer with 1 visible CPU).
 315    // This causes the worker pool in project search to spawn num_cpus - 1 = 0 workers,
 316    // which silently drops all search channels and produces zero results.
 317    server_cx.executor().set_num_cpus(1);
 318
 319    let (project, _) = init_test(&fs, cx, server_cx).await;
 320
 321    project
 322        .update(cx, |project, cx| {
 323            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 324        })
 325        .await
 326        .unwrap();
 327
 328    cx.run_until_parked();
 329
 330    do_search_and_assert(
 331        &project,
 332        "project",
 333        Default::default(),
 334        false,
 335        &[path!("project1/README.md")],
 336        cx.clone(),
 337    )
 338    .await;
 339}
 340
 341#[gpui::test]
 342async fn test_remote_project_search_inclusion(
 343    cx: &mut TestAppContext,
 344    server_cx: &mut TestAppContext,
 345) {
 346    let fs = FakeFs::new(server_cx.executor());
 347    fs.insert_tree(
 348        path!("/code"),
 349        json!({
 350            "project1": {
 351                "README.md": "# project 1",
 352            },
 353            "project2": {
 354                "README.md": "# project 2",
 355            },
 356        }),
 357    )
 358    .await;
 359
 360    let (project, _) = init_test(&fs, cx, server_cx).await;
 361
 362    project
 363        .update(cx, |project, cx| {
 364            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 365        })
 366        .await
 367        .unwrap();
 368
 369    project
 370        .update(cx, |project, cx| {
 371            project.find_or_create_worktree(path!("/code/project2"), true, cx)
 372        })
 373        .await
 374        .unwrap();
 375
 376    cx.run_until_parked();
 377
 378    // Case 1: Test search with path matcher limiting to only one worktree
 379    let path_matcher = PathMatcher::new(
 380        &["project1/*.md".to_owned()],
 381        util::paths::PathStyle::local(),
 382    )
 383    .unwrap();
 384    do_search_and_assert(
 385        &project,
 386        "project",
 387        path_matcher,
 388        true, // should be true in case of multiple worktrees
 389        &[path!("project1/README.md")],
 390        cx.clone(),
 391    )
 392    .await;
 393
 394    // Case 2: Test search without path matcher, matching both worktrees
 395    do_search_and_assert(
 396        &project,
 397        "project",
 398        Default::default(),
 399        true, // should be true in case of multiple worktrees
 400        &[path!("project1/README.md"), path!("project2/README.md")],
 401        cx.clone(),
 402    )
 403    .await;
 404}
 405
 406#[gpui::test]
 407async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 408    let fs = FakeFs::new(server_cx.executor());
 409    fs.insert_tree(
 410        "/code",
 411        json!({
 412            "project1": {
 413                ".git": {},
 414                "README.md": "# project 1",
 415                "src": {
 416                    "lib.rs": "fn one() -> usize { 1 }"
 417                }
 418            },
 419        }),
 420    )
 421    .await;
 422
 423    let (project, headless) = init_test(&fs, cx, server_cx).await;
 424
 425    cx.update_global(|settings_store: &mut SettingsStore, cx| {
 426        settings_store.set_user_settings(
 427            r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
 428            cx,
 429        )
 430    })
 431    .unwrap();
 432
 433    cx.run_until_parked();
 434
 435    server_cx.read(|cx| {
 436        assert_eq!(
 437            AllLanguageSettings::get_global(cx)
 438                .language(None, Some(&"Rust".into()), cx)
 439                .language_servers,
 440            ["from-local-settings"],
 441            "User language settings should be synchronized with the server settings"
 442        )
 443    });
 444
 445    server_cx
 446        .update_global(|settings_store: &mut SettingsStore, cx| {
 447            settings_store.set_server_settings(
 448                r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
 449                cx,
 450            )
 451        })
 452        .unwrap();
 453
 454    cx.run_until_parked();
 455
 456    server_cx.read(|cx| {
 457        assert_eq!(
 458            AllLanguageSettings::get_global(cx)
 459                .language(None, Some(&"Rust".into()), cx)
 460                .language_servers,
 461            ["from-server-settings".to_string()],
 462            "Server language settings should take precedence over the user settings"
 463        )
 464    });
 465
 466    fs.insert_tree(
 467        "/code/project1/.zed",
 468        json!({
 469            "settings.json": r#"
 470                  {
 471                    "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
 472                    "lsp": {
 473                      "override-rust-analyzer": {
 474                        "binary": {
 475                          "path": "~/.cargo/bin/rust-analyzer"
 476                        }
 477                      }
 478                    }
 479                  }"#
 480        }),
 481    )
 482    .await;
 483
 484    let worktree_id = project
 485        .update(cx, |project, cx| {
 486            project.languages().add(rust_lang());
 487            project.find_or_create_worktree("/code/project1", true, cx)
 488        })
 489        .await
 490        .unwrap()
 491        .0
 492        .read_with(cx, |worktree, _| worktree.id());
 493
 494    let buffer = project
 495        .update(cx, |project, cx| {
 496            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
 497        })
 498        .await
 499        .unwrap();
 500    cx.run_until_parked();
 501
 502    server_cx.read(|cx| {
 503        let worktree_id = headless
 504            .read(cx)
 505            .worktree_store
 506            .read(cx)
 507            .worktrees()
 508            .next()
 509            .unwrap()
 510            .read(cx)
 511            .id();
 512        assert_eq!(
 513            AllLanguageSettings::get(
 514                Some(SettingsLocation {
 515                    worktree_id,
 516                    path: rel_path("src/lib.rs")
 517                }),
 518                cx
 519            )
 520            .language(None, Some(&"Rust".into()), cx)
 521            .language_servers,
 522            ["override-rust-analyzer".to_string()]
 523        )
 524    });
 525
 526    cx.read(|cx| {
 527        assert_eq!(
 528            LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
 529            ["override-rust-analyzer".to_string()]
 530        )
 531    });
 532}
 533
 534#[gpui::test]
 535async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 536    let fs = FakeFs::new(server_cx.executor());
 537    fs.insert_tree(
 538        path!("/code"),
 539        json!({
 540            "project1": {
 541                ".git": {},
 542                "README.md": "# project 1",
 543                "src": {
 544                    "lib.rs": "fn one() -> usize { 1 }"
 545                }
 546            },
 547        }),
 548    )
 549    .await;
 550
 551    let (project, headless) = init_test(&fs, cx, server_cx).await;
 552
 553    fs.insert_tree(
 554        path!("/code/project1/.zed"),
 555        json!({
 556            "settings.json": r#"
 557          {
 558            "languages": {"Rust":{"language_servers":["rust-analyzer", "fake-analyzer"]}},
 559            "lsp": {
 560              "rust-analyzer": {
 561                "binary": {
 562                  "path": "~/.cargo/bin/rust-analyzer"
 563                }
 564              },
 565              "fake-analyzer": {
 566               "binary": {
 567                "path": "~/.cargo/bin/rust-analyzer"
 568               }
 569              }
 570            }
 571          }"#
 572        }),
 573    )
 574    .await;
 575
 576    cx.update_entity(&project, |project, _| {
 577        project.languages().register_test_language(LanguageConfig {
 578            name: "Rust".into(),
 579            matcher: LanguageMatcher {
 580                path_suffixes: vec!["rs".into()],
 581                ..Default::default()
 582            },
 583            ..Default::default()
 584        });
 585        project.languages().register_fake_lsp_adapter(
 586            "Rust",
 587            FakeLspAdapter {
 588                name: "rust-analyzer",
 589                capabilities: lsp::ServerCapabilities {
 590                    completion_provider: Some(lsp::CompletionOptions::default()),
 591                    rename_provider: Some(lsp::OneOf::Left(true)),
 592                    ..lsp::ServerCapabilities::default()
 593                },
 594                ..FakeLspAdapter::default()
 595            },
 596        );
 597        project.languages().register_fake_lsp_adapter(
 598            "Rust",
 599            FakeLspAdapter {
 600                name: "fake-analyzer",
 601                capabilities: lsp::ServerCapabilities {
 602                    completion_provider: Some(lsp::CompletionOptions::default()),
 603                    rename_provider: Some(lsp::OneOf::Left(true)),
 604                    ..lsp::ServerCapabilities::default()
 605                },
 606                ..FakeLspAdapter::default()
 607            },
 608        )
 609    });
 610
 611    let mut fake_lsp = server_cx.update(|cx| {
 612        headless.read(cx).languages.register_fake_lsp_server(
 613            LanguageServerName("rust-analyzer".into()),
 614            lsp::ServerCapabilities {
 615                completion_provider: Some(lsp::CompletionOptions::default()),
 616                rename_provider: Some(lsp::OneOf::Left(true)),
 617                ..lsp::ServerCapabilities::default()
 618            },
 619            None,
 620        )
 621    });
 622
 623    let mut fake_second_lsp = server_cx.update(|cx| {
 624        headless.read(cx).languages.register_fake_lsp_adapter(
 625            "Rust",
 626            FakeLspAdapter {
 627                name: "fake-analyzer",
 628                capabilities: lsp::ServerCapabilities {
 629                    completion_provider: Some(lsp::CompletionOptions::default()),
 630                    rename_provider: Some(lsp::OneOf::Left(true)),
 631                    ..lsp::ServerCapabilities::default()
 632                },
 633                ..FakeLspAdapter::default()
 634            },
 635        );
 636        headless.read(cx).languages.register_fake_lsp_server(
 637            LanguageServerName("fake-analyzer".into()),
 638            lsp::ServerCapabilities {
 639                completion_provider: Some(lsp::CompletionOptions::default()),
 640                rename_provider: Some(lsp::OneOf::Left(true)),
 641                ..lsp::ServerCapabilities::default()
 642            },
 643            None,
 644        )
 645    });
 646
 647    cx.run_until_parked();
 648
 649    let worktree_id = project
 650        .update(cx, |project, cx| {
 651            project.languages().add(rust_lang());
 652            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 653        })
 654        .await
 655        .unwrap()
 656        .0
 657        .read_with(cx, |worktree, _| worktree.id());
 658
 659    // Wait for the settings to synchronize
 660    cx.run_until_parked();
 661
 662    let (buffer, _handle) = project
 663        .update(cx, |project, cx| {
 664            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
 665        })
 666        .await
 667        .unwrap();
 668    cx.run_until_parked();
 669
 670    let fake_lsp = fake_lsp.next().await.unwrap();
 671    let fake_second_lsp = fake_second_lsp.next().await.unwrap();
 672
 673    cx.read(|cx| {
 674        assert_eq!(
 675            LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
 676            ["rust-analyzer".to_string(), "fake-analyzer".to_string()]
 677        )
 678    });
 679
 680    let buffer_id = cx.read(|cx| {
 681        let buffer = buffer.read(cx);
 682        assert_eq!(buffer.language().unwrap().name(), "Rust");
 683        buffer.remote_id()
 684    });
 685
 686    server_cx.read(|cx| {
 687        let buffer = headless
 688            .read(cx)
 689            .buffer_store
 690            .read(cx)
 691            .get(buffer_id)
 692            .unwrap();
 693
 694        assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust");
 695    });
 696
 697    server_cx.read(|cx| {
 698        let lsp_store = headless.read(cx).lsp_store.read(cx);
 699        assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 2);
 700    });
 701
 702    fake_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
 703        Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
 704            label: "boop".to_string(),
 705            ..Default::default()
 706        }])))
 707    });
 708
 709    fake_second_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
 710        Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
 711            label: "beep".to_string(),
 712            ..Default::default()
 713        }])))
 714    });
 715
 716    let result = project
 717        .update(cx, |project, cx| {
 718            project.completions(
 719                &buffer,
 720                0,
 721                CompletionContext {
 722                    trigger_kind: CompletionTriggerKind::INVOKED,
 723                    trigger_character: None,
 724                },
 725                cx,
 726            )
 727        })
 728        .await
 729        .unwrap();
 730
 731    assert_eq!(
 732        result
 733            .into_iter()
 734            .flat_map(|response| response.completions)
 735            .map(|c| c.label.text)
 736            .collect::<Vec<_>>(),
 737        vec!["boop".to_string(), "beep".to_string()]
 738    );
 739
 740    fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {
 741        Ok(Some(lsp::WorkspaceEdit {
 742            changes: Some(
 743                [(
 744                    lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
 745                    vec![lsp::TextEdit::new(
 746                        lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
 747                        "two".to_string(),
 748                    )],
 749                )]
 750                .into_iter()
 751                .collect(),
 752            ),
 753            ..Default::default()
 754        }))
 755    });
 756
 757    project
 758        .update(cx, |project, cx| {
 759            project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
 760        })
 761        .await
 762        .unwrap();
 763
 764    cx.run_until_parked();
 765    buffer.update(cx, |buffer, _| {
 766        assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
 767    })
 768}
 769
 770#[gpui::test]
 771async fn test_remote_cancel_language_server_work(
 772    cx: &mut TestAppContext,
 773    server_cx: &mut TestAppContext,
 774) {
 775    let fs = FakeFs::new(server_cx.executor());
 776    fs.insert_tree(
 777        path!("/code"),
 778        json!({
 779            "project1": {
 780                ".git": {},
 781                "README.md": "# project 1",
 782                "src": {
 783                    "lib.rs": "fn one() -> usize { 1 }"
 784                }
 785            },
 786        }),
 787    )
 788    .await;
 789
 790    let (project, headless) = init_test(&fs, cx, server_cx).await;
 791
 792    fs.insert_tree(
 793        path!("/code/project1/.zed"),
 794        json!({
 795            "settings.json": r#"
 796          {
 797            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
 798            "lsp": {
 799              "rust-analyzer": {
 800                "binary": {
 801                  "path": "~/.cargo/bin/rust-analyzer"
 802                }
 803              }
 804            }
 805          }"#
 806        }),
 807    )
 808    .await;
 809
 810    cx.update_entity(&project, |project, _| {
 811        project.languages().register_test_language(LanguageConfig {
 812            name: "Rust".into(),
 813            matcher: LanguageMatcher {
 814                path_suffixes: vec!["rs".into()],
 815                ..Default::default()
 816            },
 817            ..Default::default()
 818        });
 819        project.languages().register_fake_lsp_adapter(
 820            "Rust",
 821            FakeLspAdapter {
 822                name: "rust-analyzer",
 823                ..Default::default()
 824            },
 825        )
 826    });
 827
 828    let mut fake_lsp = server_cx.update(|cx| {
 829        headless.read(cx).languages.register_fake_lsp_server(
 830            LanguageServerName("rust-analyzer".into()),
 831            Default::default(),
 832            None,
 833        )
 834    });
 835
 836    cx.run_until_parked();
 837
 838    let worktree_id = project
 839        .update(cx, |project, cx| {
 840            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 841        })
 842        .await
 843        .unwrap()
 844        .0
 845        .read_with(cx, |worktree, _| worktree.id());
 846
 847    cx.run_until_parked();
 848
 849    let (buffer, _handle) = project
 850        .update(cx, |project, cx| {
 851            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
 852        })
 853        .await
 854        .unwrap();
 855
 856    cx.run_until_parked();
 857
 858    let mut fake_lsp = fake_lsp.next().await.unwrap();
 859
 860    // Cancelling all language server work for a given buffer
 861    {
 862        // Two operations, one cancellable and one not.
 863        fake_lsp
 864            .start_progress_with(
 865                "another-token",
 866                lsp::WorkDoneProgressBegin {
 867                    cancellable: Some(false),
 868                    ..Default::default()
 869                },
 870                DEFAULT_LSP_REQUEST_TIMEOUT,
 871            )
 872            .await;
 873
 874        let progress_token = "the-progress-token";
 875        fake_lsp
 876            .start_progress_with(
 877                progress_token,
 878                lsp::WorkDoneProgressBegin {
 879                    cancellable: Some(true),
 880                    ..Default::default()
 881                },
 882                DEFAULT_LSP_REQUEST_TIMEOUT,
 883            )
 884            .await;
 885
 886        cx.executor().run_until_parked();
 887
 888        project.update(cx, |project, cx| {
 889            project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
 890        });
 891
 892        cx.executor().run_until_parked();
 893
 894        // Verify the cancellation was received on the server side
 895        let cancel_notification = fake_lsp
 896            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
 897            .await;
 898        assert_eq!(
 899            cancel_notification.token,
 900            lsp::NumberOrString::String(progress_token.into())
 901        );
 902    }
 903
 904    // Cancelling work by server_id and token
 905    {
 906        let server_id = fake_lsp.server.server_id();
 907        let progress_token = "the-progress-token";
 908
 909        fake_lsp
 910            .start_progress_with(
 911                progress_token,
 912                lsp::WorkDoneProgressBegin {
 913                    cancellable: Some(true),
 914                    ..Default::default()
 915                },
 916                DEFAULT_LSP_REQUEST_TIMEOUT,
 917            )
 918            .await;
 919
 920        cx.executor().run_until_parked();
 921
 922        project.update(cx, |project, cx| {
 923            project.cancel_language_server_work(
 924                server_id,
 925                Some(ProgressToken::String(SharedString::from(progress_token))),
 926                cx,
 927            )
 928        });
 929
 930        cx.executor().run_until_parked();
 931
 932        // Verify the cancellation was received on the server side
 933        let cancel_notification = fake_lsp
 934            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
 935            .await;
 936        assert_eq!(
 937            cancel_notification.token,
 938            lsp::NumberOrString::String(progress_token.to_owned())
 939        );
 940    }
 941}
 942
 943#[gpui::test]
 944async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 945    let fs = FakeFs::new(server_cx.executor());
 946    fs.insert_tree(
 947        path!("/code"),
 948        json!({
 949            "project1": {
 950                ".git": {},
 951                "README.md": "# project 1",
 952                "src": {
 953                    "lib.rs": "fn one() -> usize { 1 }"
 954                }
 955            },
 956        }),
 957    )
 958    .await;
 959
 960    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 961    let (worktree, _) = project
 962        .update(cx, |project, cx| {
 963            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 964        })
 965        .await
 966        .unwrap();
 967
 968    let worktree_id = cx.update(|cx| worktree.read(cx).id());
 969
 970    let buffer = project
 971        .update(cx, |project, cx| {
 972            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
 973        })
 974        .await
 975        .unwrap();
 976
 977    fs.save(
 978        &PathBuf::from(path!("/code/project1/src/lib.rs")),
 979        &("bangles".to_string().into()),
 980        LineEnding::Unix,
 981    )
 982    .await
 983    .unwrap();
 984
 985    cx.run_until_parked();
 986
 987    buffer.update(cx, |buffer, cx| {
 988        assert_eq!(buffer.text(), "bangles");
 989        buffer.edit([(0..0, "a")], None, cx);
 990    });
 991
 992    fs.save(
 993        &PathBuf::from(path!("/code/project1/src/lib.rs")),
 994        &("bloop".to_string().into()),
 995        LineEnding::Unix,
 996    )
 997    .await
 998    .unwrap();
 999
1000    cx.run_until_parked();
1001    cx.update(|cx| {
1002        assert!(buffer.read(cx).has_conflict());
1003    });
1004
1005    project
1006        .update(cx, |project, cx| {
1007            project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
1008        })
1009        .await
1010        .unwrap();
1011    cx.run_until_parked();
1012
1013    cx.update(|cx| {
1014        assert!(!buffer.read(cx).has_conflict());
1015    });
1016}
1017
1018#[gpui::test]
1019async fn test_remote_resolve_path_in_buffer(
1020    cx: &mut TestAppContext,
1021    server_cx: &mut TestAppContext,
1022) {
1023    let fs = FakeFs::new(server_cx.executor());
1024    // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
1025    fs.insert_tree(
1026        path!("/code"),
1027        json!({
1028            "project1": {
1029                ".git": {},
1030                "README.md": "# project 1",
1031                "src": {
1032                    "lib.rs": "fn one() -> usize { 1 }"
1033                }
1034            },
1035            "project2": {
1036                ".git": {},
1037                "README.md": "# project 2",
1038                "src": {
1039                    "lib.rs": "fn two() -> usize { 2 }"
1040                }
1041            }
1042        }),
1043    )
1044    .await;
1045
1046    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1047
1048    let _ = project
1049        .update(cx, |project, cx| {
1050            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1051        })
1052        .await
1053        .unwrap();
1054
1055    let (worktree2, _) = project
1056        .update(cx, |project, cx| {
1057            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1058        })
1059        .await
1060        .unwrap();
1061
1062    let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
1063
1064    cx.run_until_parked();
1065
1066    let buffer2 = project
1067        .update(cx, |project, cx| {
1068            project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx)
1069        })
1070        .await
1071        .unwrap();
1072
1073    let path = project
1074        .update(cx, |project, cx| {
1075            project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
1076        })
1077        .await
1078        .unwrap();
1079    assert!(path.is_file());
1080    assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md"));
1081
1082    let path = project
1083        .update(cx, |project, cx| {
1084            project.resolve_path_in_buffer("../README.md", &buffer2, cx)
1085        })
1086        .await
1087        .unwrap();
1088    assert!(path.is_file());
1089    assert_eq!(
1090        path.project_path().unwrap().clone(),
1091        (worktree2_id, rel_path("README.md")).into()
1092    );
1093
1094    let path = project
1095        .update(cx, |project, cx| {
1096            project.resolve_path_in_buffer("../src", &buffer2, cx)
1097        })
1098        .await
1099        .unwrap();
1100    assert_eq!(
1101        path.project_path().unwrap().clone(),
1102        (worktree2_id, rel_path("src")).into()
1103    );
1104    assert!(path.is_dir());
1105}
1106
1107#[gpui::test]
1108async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1109    let fs = FakeFs::new(server_cx.executor());
1110    fs.insert_tree(
1111        path!("/code"),
1112        json!({
1113            "project1": {
1114                ".git": {},
1115                "README.md": "# project 1",
1116                "src": {
1117                    "lib.rs": "fn one() -> usize { 1 }"
1118                }
1119            },
1120        }),
1121    )
1122    .await;
1123
1124    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1125
1126    let path = project
1127        .update(cx, |project, cx| {
1128            project.resolve_abs_path(path!("/code/project1/README.md"), cx)
1129        })
1130        .await
1131        .unwrap();
1132
1133    assert!(path.is_file());
1134    assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md"));
1135
1136    let path = project
1137        .update(cx, |project, cx| {
1138            project.resolve_abs_path(path!("/code/project1/src"), cx)
1139        })
1140        .await
1141        .unwrap();
1142
1143    assert!(path.is_dir());
1144    assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src"));
1145
1146    let path = project
1147        .update(cx, |project, cx| {
1148            project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
1149        })
1150        .await;
1151    assert!(path.is_none());
1152}
1153
1154#[gpui::test(iterations = 10)]
1155async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1156    let fs = FakeFs::new(server_cx.executor());
1157    fs.insert_tree(
1158        "/code",
1159        json!({
1160            "project1": {
1161                ".git": {},
1162                "README.md": "# project 1",
1163                "src": {
1164                    "lib.rs": "fn one() -> usize { 1 }"
1165                }
1166            },
1167        }),
1168    )
1169    .await;
1170
1171    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1172    let (worktree, _) = project
1173        .update(cx, |project, cx| {
1174            project.find_or_create_worktree("/code/project1", true, cx)
1175        })
1176        .await
1177        .unwrap();
1178    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
1179
1180    // Open a buffer on the client but cancel after a random amount of time.
1181    let buffer = project.update(cx, |p, cx| {
1182        p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1183    });
1184    cx.executor().simulate_random_delay().await;
1185    drop(buffer);
1186
1187    // Try opening the same buffer again as the client, and ensure we can
1188    // still do it despite the cancellation above.
1189    let buffer = project
1190        .update(cx, |p, cx| {
1191            p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1192        })
1193        .await
1194        .unwrap();
1195
1196    buffer.read_with(cx, |buf, _| {
1197        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
1198    });
1199}
1200
1201#[gpui::test]
1202async fn test_adding_then_removing_then_adding_worktrees(
1203    cx: &mut TestAppContext,
1204    server_cx: &mut TestAppContext,
1205) {
1206    let fs = FakeFs::new(server_cx.executor());
1207    fs.insert_tree(
1208        path!("/code"),
1209        json!({
1210            "project1": {
1211                ".git": {},
1212                "README.md": "# project 1",
1213                "src": {
1214                    "lib.rs": "fn one() -> usize { 1 }"
1215                }
1216            },
1217            "project2": {
1218                "README.md": "# project 2",
1219            },
1220        }),
1221    )
1222    .await;
1223
1224    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1225    let (_worktree, _) = project
1226        .update(cx, |project, cx| {
1227            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1228        })
1229        .await
1230        .unwrap();
1231
1232    let (worktree_2, _) = project
1233        .update(cx, |project, cx| {
1234            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1235        })
1236        .await
1237        .unwrap();
1238    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1239
1240    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1241
1242    let (worktree_2, _) = project
1243        .update(cx, |project, cx| {
1244            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1245        })
1246        .await
1247        .unwrap();
1248
1249    cx.run_until_parked();
1250    worktree_2.update(cx, |worktree, _cx| {
1251        assert!(worktree.is_visible());
1252        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1253        assert_eq!(entries.len(), 2);
1254        assert_eq!(entries[1].path.as_unix_str(), "README.md")
1255    })
1256}
1257
1258#[gpui::test]
1259async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1260    let fs = FakeFs::new(server_cx.executor());
1261    fs.insert_tree(
1262        path!("/code"),
1263        json!({
1264            "project1": {
1265                ".git": {},
1266                "README.md": "# project 1",
1267                "src": {
1268                    "lib.rs": "fn one() -> usize { 1 }"
1269                }
1270            },
1271        }),
1272    )
1273    .await;
1274
1275    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1276    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1277    cx.executor().run_until_parked();
1278
1279    let buffer = buffer.await.unwrap();
1280
1281    cx.update(|cx| {
1282        assert_eq!(
1283            buffer.read(cx).text(),
1284            initial_server_settings_content()
1285                .to_string()
1286                .replace("\r\n", "\n")
1287        )
1288    })
1289}
1290
1291#[gpui::test(iterations = 20)]
1292async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1293    let fs = FakeFs::new(server_cx.executor());
1294    fs.insert_tree(
1295        path!("/code"),
1296        json!({
1297            "project1": {
1298                ".git": {},
1299                "README.md": "# project 1",
1300                "src": {
1301                    "lib.rs": "fn one() -> usize { 1 }"
1302                }
1303            },
1304        }),
1305    )
1306    .await;
1307
1308    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1309
1310    let (worktree, _) = project
1311        .update(cx, |project, cx| {
1312            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1313        })
1314        .await
1315        .unwrap();
1316
1317    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1318    let buffer = project
1319        .update(cx, |project, cx| {
1320            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1321        })
1322        .await
1323        .unwrap();
1324
1325    buffer.update(cx, |buffer, cx| {
1326        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1327        let ix = buffer.text().find('1').unwrap();
1328        buffer.edit([(ix..ix + 1, "100")], None, cx);
1329    });
1330
1331    let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
1332    client
1333        .update(cx, |client, cx| client.simulate_disconnect(cx))
1334        .detach();
1335
1336    project
1337        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1338        .await
1339        .unwrap();
1340
1341    assert_eq!(
1342        fs.load(path!("/code/project1/src/lib.rs").as_ref())
1343            .await
1344            .unwrap(),
1345        "fn one() -> usize { 100 }"
1346    );
1347}
1348
1349#[gpui::test]
1350async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1351    let fs = FakeFs::new(server_cx.executor());
1352    fs.insert_tree(
1353        "/code",
1354        json!({
1355            "project1": {
1356                ".git": {},
1357                "README.md": "# project 1",
1358            },
1359        }),
1360    )
1361    .await;
1362
1363    let (project, _) = init_test(&fs, cx, server_cx).await;
1364
1365    let (worktree, _) = project
1366        .update(cx, |project, cx| {
1367            project.find_or_create_worktree("/code/project1", true, cx)
1368        })
1369        .await
1370        .unwrap();
1371
1372    cx.run_until_parked();
1373
1374    fs.rename(
1375        &PathBuf::from("/code/project1"),
1376        &PathBuf::from("/code/project2"),
1377        Default::default(),
1378    )
1379    .await
1380    .unwrap();
1381
1382    cx.run_until_parked();
1383    worktree.update(cx, |worktree, _| {
1384        assert_eq!(worktree.root_name(), "project2")
1385    })
1386}
1387
1388#[gpui::test]
1389async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1390    let fs = FakeFs::new(server_cx.executor());
1391    fs.insert_tree(
1392        "/code",
1393        json!({
1394            "project1": {
1395                ".git": {},
1396                "README.md": "# project 1",
1397            },
1398        }),
1399    )
1400    .await;
1401
1402    let (project, _) = init_test(&fs, cx, server_cx).await;
1403    let (worktree, _) = project
1404        .update(cx, |project, cx| {
1405            project.find_or_create_worktree("/code/project1", true, cx)
1406        })
1407        .await
1408        .unwrap();
1409
1410    cx.run_until_parked();
1411
1412    let entry = project
1413        .update(cx, |project, cx| {
1414            let worktree = worktree.read(cx);
1415            let entry = worktree.entry_for_path(rel_path("README.md")).unwrap();
1416            project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx)
1417        })
1418        .await
1419        .unwrap()
1420        .into_included()
1421        .unwrap();
1422
1423    cx.run_until_parked();
1424
1425    worktree.update(cx, |worktree, _| {
1426        assert_eq!(
1427            worktree.entry_for_path(rel_path("README.rst")).unwrap().id,
1428            entry.id
1429        )
1430    });
1431}
1432
1433#[gpui::test]
1434async fn test_copy_file_into_remote_project(
1435    cx: &mut TestAppContext,
1436    server_cx: &mut TestAppContext,
1437) {
1438    let remote_fs = FakeFs::new(server_cx.executor());
1439    remote_fs
1440        .insert_tree(
1441            path!("/code"),
1442            json!({
1443                "project1": {
1444                    ".git": {},
1445                    "README.md": "# project 1",
1446                    "src": {
1447                        "main.rs": ""
1448                    }
1449                },
1450            }),
1451        )
1452        .await;
1453
1454    let (project, _) = init_test(&remote_fs, cx, server_cx).await;
1455    let (worktree, _) = project
1456        .update(cx, |project, cx| {
1457            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1458        })
1459        .await
1460        .unwrap();
1461
1462    cx.run_until_parked();
1463
1464    let local_fs = project
1465        .read_with(cx, |project, _| project.fs().clone())
1466        .as_fake();
1467    local_fs
1468        .insert_tree(
1469            path!("/local-code"),
1470            json!({
1471                "dir1": {
1472                    "file1": "file 1 content",
1473                    "dir2": {
1474                        "file2": "file 2 content",
1475                        "dir3": {
1476                            "file3": ""
1477                        },
1478                        "dir4": {}
1479                    },
1480                    "dir5": {}
1481                },
1482                "file4": "file 4 content"
1483            }),
1484        )
1485        .await;
1486
1487    worktree
1488        .update(cx, |worktree, cx| {
1489            worktree.copy_external_entries(
1490                rel_path("src").into(),
1491                vec![
1492                    Path::new(path!("/local-code/dir1/file1")).into(),
1493                    Path::new(path!("/local-code/dir1/dir2")).into(),
1494                ],
1495                local_fs.clone(),
1496                cx,
1497            )
1498        })
1499        .await
1500        .unwrap();
1501
1502    assert_eq!(
1503        remote_fs.paths(true),
1504        vec![
1505            PathBuf::from(path!("/")),
1506            PathBuf::from(path!("/code")),
1507            PathBuf::from(path!("/code/project1")),
1508            PathBuf::from(path!("/code/project1/.git")),
1509            PathBuf::from(path!("/code/project1/README.md")),
1510            PathBuf::from(path!("/code/project1/src")),
1511            PathBuf::from(path!("/code/project1/src/dir2")),
1512            PathBuf::from(path!("/code/project1/src/file1")),
1513            PathBuf::from(path!("/code/project1/src/main.rs")),
1514            PathBuf::from(path!("/code/project1/src/dir2/dir3")),
1515            PathBuf::from(path!("/code/project1/src/dir2/dir4")),
1516            PathBuf::from(path!("/code/project1/src/dir2/file2")),
1517            PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
1518        ]
1519    );
1520    assert_eq!(
1521        remote_fs
1522            .load(path!("/code/project1/src/file1").as_ref())
1523            .await
1524            .unwrap(),
1525        "file 1 content"
1526    );
1527    assert_eq!(
1528        remote_fs
1529            .load(path!("/code/project1/src/dir2/file2").as_ref())
1530            .await
1531            .unwrap(),
1532        "file 2 content"
1533    );
1534    assert_eq!(
1535        remote_fs
1536            .load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
1537            .await
1538            .unwrap(),
1539        ""
1540    );
1541}
1542
1543#[gpui::test]
1544async fn test_remote_root_repo_common_dir(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1545    let fs = FakeFs::new(server_cx.executor());
1546    fs.insert_tree(
1547        "/code",
1548        json!({
1549            "main_repo": {
1550                ".git": {},
1551                "file.txt": "content",
1552            },
1553            "no_git": {
1554                "file.txt": "content",
1555            },
1556        }),
1557    )
1558    .await;
1559
1560    // Create a linked worktree that points back to main_repo's .git.
1561    fs.add_linked_worktree_for_repo(
1562        Path::new("/code/main_repo/.git"),
1563        false,
1564        GitWorktree {
1565            path: PathBuf::from("/code/linked_worktree"),
1566            ref_name: Some("refs/heads/feature-branch".into()),
1567            sha: "abc123".into(),
1568            is_main: false,
1569            is_bare: false,
1570        },
1571    )
1572    .await;
1573
1574    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1575
1576    // Main repo: root_repo_common_dir should be the .git directory itself.
1577    let (worktree_main, _) = project
1578        .update(cx, |project, cx| {
1579            project.find_or_create_worktree("/code/main_repo", true, cx)
1580        })
1581        .await
1582        .unwrap();
1583    cx.executor().run_until_parked();
1584
1585    let common_dir = worktree_main.read_with(cx, |worktree, _| {
1586        worktree.snapshot().root_repo_common_dir().cloned()
1587    });
1588    assert_eq!(
1589        common_dir.as_deref(),
1590        Some(Path::new("/code/main_repo/.git")),
1591    );
1592
1593    // Linked worktree: root_repo_common_dir should point to the main repo's .git.
1594    let (worktree_linked, _) = project
1595        .update(cx, |project, cx| {
1596            project.find_or_create_worktree("/code/linked_worktree", true, cx)
1597        })
1598        .await
1599        .unwrap();
1600    cx.executor().run_until_parked();
1601
1602    let common_dir = worktree_linked.read_with(cx, |worktree, _| {
1603        worktree.snapshot().root_repo_common_dir().cloned()
1604    });
1605    assert_eq!(
1606        common_dir.as_deref(),
1607        Some(Path::new("/code/main_repo/.git")),
1608    );
1609
1610    // No git repo: root_repo_common_dir should be None.
1611    let (worktree_no_git, _) = project
1612        .update(cx, |project, cx| {
1613            project.find_or_create_worktree("/code/no_git", true, cx)
1614        })
1615        .await
1616        .unwrap();
1617    cx.executor().run_until_parked();
1618
1619    let common_dir = worktree_no_git.read_with(cx, |worktree, _| {
1620        worktree.snapshot().root_repo_common_dir().cloned()
1621    });
1622    assert_eq!(common_dir, None);
1623}
1624
1625#[gpui::test]
1626async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1627    let text_2 = "
1628        fn one() -> usize {
1629            1
1630        }
1631    "
1632    .unindent();
1633    let text_1 = "
1634        fn one() -> usize {
1635            0
1636        }
1637    "
1638    .unindent();
1639
1640    let fs = FakeFs::new(server_cx.executor());
1641    fs.insert_tree(
1642        "/code",
1643        json!({
1644            "project1": {
1645                ".git": {},
1646                "src": {
1647                    "lib.rs": text_2
1648                },
1649                "README.md": "# project 1",
1650            },
1651        }),
1652    )
1653    .await;
1654    fs.set_index_for_repo(
1655        Path::new("/code/project1/.git"),
1656        &[("src/lib.rs", text_1.clone())],
1657    );
1658    fs.set_head_for_repo(
1659        Path::new("/code/project1/.git"),
1660        &[("src/lib.rs", text_1.clone())],
1661        "deadbeef",
1662    );
1663
1664    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1665    let (worktree, _) = project
1666        .update(cx, |project, cx| {
1667            project.find_or_create_worktree("/code/project1", true, cx)
1668        })
1669        .await
1670        .unwrap();
1671    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1672    cx.executor().run_until_parked();
1673
1674    let buffer = project
1675        .update(cx, |project, cx| {
1676            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1677        })
1678        .await
1679        .unwrap();
1680    let diff = project
1681        .update(cx, |project, cx| {
1682            project.open_uncommitted_diff(buffer.clone(), cx)
1683        })
1684        .await
1685        .unwrap();
1686
1687    diff.read_with(cx, |diff, cx| {
1688        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1689        assert_eq!(
1690            diff.secondary_diff()
1691                .unwrap()
1692                .read(cx)
1693                .base_text_string(cx)
1694                .unwrap(),
1695            text_1
1696        );
1697    });
1698
1699    // stage the current buffer's contents
1700    fs.set_index_for_repo(
1701        Path::new("/code/project1/.git"),
1702        &[("src/lib.rs", text_2.clone())],
1703    );
1704
1705    cx.executor().run_until_parked();
1706    diff.read_with(cx, |diff, cx| {
1707        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1708        assert_eq!(
1709            diff.secondary_diff()
1710                .unwrap()
1711                .read(cx)
1712                .base_text_string(cx)
1713                .unwrap(),
1714            text_2
1715        );
1716    });
1717
1718    // commit the current buffer's contents
1719    fs.set_head_for_repo(
1720        Path::new("/code/project1/.git"),
1721        &[("src/lib.rs", text_2.clone())],
1722        "deadbeef",
1723    );
1724
1725    cx.executor().run_until_parked();
1726    diff.read_with(cx, |diff, cx| {
1727        assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
1728        assert_eq!(
1729            diff.secondary_diff()
1730                .unwrap()
1731                .read(cx)
1732                .base_text_string(cx)
1733                .unwrap(),
1734            text_2
1735        );
1736    });
1737}
1738
1739#[gpui::test]
1740async fn test_remote_git_diffs_when_recv_update_repository_delay(
1741    cx: &mut TestAppContext,
1742    server_cx: &mut TestAppContext,
1743) {
1744    cx.update(|cx| {
1745        let settings_store = SettingsStore::test(cx);
1746        cx.set_global(settings_store);
1747        theme_settings::init(theme::LoadThemes::JustBase, cx);
1748        release_channel::init(semver::Version::new(0, 0, 0), cx);
1749        editor::init(cx);
1750    });
1751
1752    use editor::Editor;
1753    use gpui::VisualContext;
1754    let text_2 = "
1755        fn one() -> usize {
1756            1
1757        }
1758    "
1759    .unindent();
1760    let text_1 = "
1761        fn one() -> usize {
1762            0
1763        }
1764    "
1765    .unindent();
1766
1767    let fs = FakeFs::new(server_cx.executor());
1768    fs.insert_tree(
1769        path!("/code"),
1770        json!({
1771            "project1": {
1772                "src": {
1773                    "lib.rs": text_2
1774                },
1775                "README.md": "# project 1",
1776            },
1777        }),
1778    )
1779    .await;
1780
1781    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1782    let (worktree, _) = project
1783        .update(cx, |project, cx| {
1784            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1785        })
1786        .await
1787        .unwrap();
1788    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1789    let buffer = project
1790        .update(cx, |project, cx| {
1791            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1792        })
1793        .await
1794        .unwrap();
1795    let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
1796
1797    let cx = cx.add_empty_window();
1798    let editor = cx.new_window_entity(|window, cx| {
1799        Editor::for_buffer(buffer, Some(project.clone()), window, cx)
1800    });
1801
1802    // Remote server will send proto::UpdateRepository after the instance of Editor create.
1803    fs.insert_tree(
1804        path!("/code"),
1805        json!({
1806            "project1": {
1807                ".git": {},
1808            },
1809        }),
1810    )
1811    .await;
1812
1813    fs.set_index_for_repo(
1814        Path::new(path!("/code/project1/.git")),
1815        &[("src/lib.rs", text_1.clone())],
1816    );
1817    fs.set_head_for_repo(
1818        Path::new(path!("/code/project1/.git")),
1819        &[("src/lib.rs", text_1.clone())],
1820        "sha",
1821    );
1822
1823    cx.executor().run_until_parked();
1824    let diff = editor
1825        .read_with(cx, |editor, cx| {
1826            editor
1827                .buffer()
1828                .read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
1829        })
1830        .unwrap();
1831
1832    diff.read_with(cx, |diff, cx| {
1833        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1834        assert_eq!(
1835            diff.secondary_diff()
1836                .unwrap()
1837                .read(cx)
1838                .base_text_string(cx)
1839                .unwrap(),
1840            text_1
1841        );
1842    });
1843
1844    // stage the current buffer's contents
1845    fs.set_index_for_repo(
1846        Path::new(path!("/code/project1/.git")),
1847        &[("src/lib.rs", text_2.clone())],
1848    );
1849
1850    cx.executor().run_until_parked();
1851    diff.read_with(cx, |diff, cx| {
1852        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1853        assert_eq!(
1854            diff.secondary_diff()
1855                .unwrap()
1856                .read(cx)
1857                .base_text_string(cx)
1858                .unwrap(),
1859            text_2
1860        );
1861    });
1862
1863    // commit the current buffer's contents
1864    fs.set_head_for_repo(
1865        Path::new(path!("/code/project1/.git")),
1866        &[("src/lib.rs", text_2.clone())],
1867        "sha",
1868    );
1869
1870    cx.executor().run_until_parked();
1871    diff.read_with(cx, |diff, cx| {
1872        assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
1873        assert_eq!(
1874            diff.secondary_diff()
1875                .unwrap()
1876                .read(cx)
1877                .base_text_string(cx)
1878                .unwrap(),
1879            text_2
1880        );
1881    });
1882}
1883
1884#[gpui::test]
1885async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1886    let fs = FakeFs::new(server_cx.executor());
1887    fs.insert_tree(
1888        path!("/code"),
1889        json!({
1890            "project1": {
1891                ".git": {},
1892                "README.md": "# project 1",
1893            },
1894        }),
1895    )
1896    .await;
1897
1898    let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1899    let branches = ["main", "dev", "feature-1"];
1900    let branches_set = branches
1901        .iter()
1902        .map(ToString::to_string)
1903        .collect::<HashSet<_>>();
1904    fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1905
1906    let (_worktree, _) = project
1907        .update(cx, |project, cx| {
1908            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1909        })
1910        .await
1911        .unwrap();
1912    // Give the worktree a bit of time to index the file system
1913    cx.run_until_parked();
1914
1915    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1916
1917    let remote_branches = repository
1918        .update(cx, |repository, _| repository.branches())
1919        .await
1920        .unwrap()
1921        .unwrap();
1922
1923    let new_branch = branches[2];
1924
1925    let remote_branches = remote_branches
1926        .into_iter()
1927        .map(|branch| branch.name().to_string())
1928        .collect::<HashSet<_>>();
1929
1930    assert_eq!(&remote_branches, &branches_set);
1931
1932    cx.update(|cx| {
1933        repository.update(cx, |repository, _cx| {
1934            repository.change_branch(new_branch.to_string())
1935        })
1936    })
1937    .await
1938    .unwrap()
1939    .unwrap();
1940
1941    cx.run_until_parked();
1942
1943    let server_branch = server_cx.update(|cx| {
1944        headless_project.update(cx, |headless_project, cx| {
1945            headless_project.git_store.update(cx, |git_store, cx| {
1946                git_store
1947                    .repositories()
1948                    .values()
1949                    .next()
1950                    .unwrap()
1951                    .read(cx)
1952                    .branch
1953                    .as_ref()
1954                    .unwrap()
1955                    .clone()
1956            })
1957        })
1958    });
1959
1960    assert_eq!(server_branch.name(), branches[2]);
1961
1962    // Also try creating a new branch
1963    cx.update(|cx| {
1964        repository.update(cx, |repo, _cx| {
1965            repo.create_branch("totally-new-branch".to_string(), None)
1966        })
1967    })
1968    .await
1969    .unwrap()
1970    .unwrap();
1971
1972    cx.update(|cx| {
1973        repository.update(cx, |repo, _cx| {
1974            repo.change_branch("totally-new-branch".to_string())
1975        })
1976    })
1977    .await
1978    .unwrap()
1979    .unwrap();
1980
1981    cx.run_until_parked();
1982
1983    let server_branch = server_cx.update(|cx| {
1984        headless_project.update(cx, |headless_project, cx| {
1985            headless_project.git_store.update(cx, |git_store, cx| {
1986                git_store
1987                    .repositories()
1988                    .values()
1989                    .next()
1990                    .unwrap()
1991                    .read(cx)
1992                    .branch
1993                    .as_ref()
1994                    .unwrap()
1995                    .clone()
1996            })
1997        })
1998    });
1999
2000    assert_eq!(server_branch.name(), "totally-new-branch");
2001}
2002
2003#[gpui::test]
2004async fn test_remote_git_checkpoints(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2005    let fs = FakeFs::new(server_cx.executor());
2006    fs.insert_tree(
2007        path!("/code"),
2008        json!({
2009            "project1": {
2010                ".git": {},
2011                "file.txt": "original content",
2012            },
2013        }),
2014    )
2015    .await;
2016
2017    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2018
2019    let (_worktree, _) = project
2020        .update(cx, |project, cx| {
2021            project.find_or_create_worktree(path!("/code/project1"), true, cx)
2022        })
2023        .await
2024        .unwrap();
2025    cx.run_until_parked();
2026
2027    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
2028
2029    // 1. Create a checkpoint of the original state
2030    let checkpoint_1 = repository
2031        .update(cx, |repository, _| repository.checkpoint())
2032        .await
2033        .unwrap()
2034        .unwrap();
2035
2036    // 2. Modify a file on the server-side fs
2037    fs.write(
2038        Path::new(path!("/code/project1/file.txt")),
2039        b"modified content",
2040    )
2041    .await
2042    .unwrap();
2043
2044    // 3. Create a second checkpoint with the modified state
2045    let checkpoint_2 = repository
2046        .update(cx, |repository, _| repository.checkpoint())
2047        .await
2048        .unwrap()
2049        .unwrap();
2050
2051    // 4. compare_checkpoints: same checkpoint with itself => equal
2052    let equal = repository
2053        .update(cx, |repository, _| {
2054            repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
2055        })
2056        .await
2057        .unwrap()
2058        .unwrap();
2059    assert!(equal, "a checkpoint compared with itself should be equal");
2060
2061    // 5. compare_checkpoints: different states => not equal
2062    let equal = repository
2063        .update(cx, |repository, _| {
2064            repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
2065        })
2066        .await
2067        .unwrap()
2068        .unwrap();
2069    assert!(
2070        !equal,
2071        "checkpoints of different states should not be equal"
2072    );
2073
2074    // 6. diff_checkpoints: same checkpoint => empty diff
2075    let diff = repository
2076        .update(cx, |repository, _| {
2077            repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
2078        })
2079        .await
2080        .unwrap()
2081        .unwrap();
2082    assert!(
2083        diff.is_empty(),
2084        "diff of identical checkpoints should be empty"
2085    );
2086
2087    // 7. diff_checkpoints: different checkpoints => non-empty diff mentioning the changed file
2088    let diff = repository
2089        .update(cx, |repository, _| {
2090            repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
2091        })
2092        .await
2093        .unwrap()
2094        .unwrap();
2095    assert!(
2096        !diff.is_empty(),
2097        "diff of different checkpoints should be non-empty"
2098    );
2099    assert!(
2100        diff.contains("file.txt"),
2101        "diff should mention the changed file"
2102    );
2103    assert!(
2104        diff.contains("original content"),
2105        "diff should contain removed content"
2106    );
2107    assert!(
2108        diff.contains("modified content"),
2109        "diff should contain added content"
2110    );
2111
2112    // 8. restore_checkpoint: restore to original state
2113    repository
2114        .update(cx, |repository, _| {
2115            repository.restore_checkpoint(checkpoint_1.clone())
2116        })
2117        .await
2118        .unwrap()
2119        .unwrap();
2120    cx.run_until_parked();
2121
2122    // 9. Create a checkpoint after restore
2123    let checkpoint_3 = repository
2124        .update(cx, |repository, _| repository.checkpoint())
2125        .await
2126        .unwrap()
2127        .unwrap();
2128
2129    // 10. compare_checkpoints: restored state matches original
2130    let equal = repository
2131        .update(cx, |repository, _| {
2132            repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
2133        })
2134        .await
2135        .unwrap()
2136        .unwrap();
2137    assert!(equal, "restored state should match original checkpoint");
2138
2139    // 11. diff_checkpoints: restored state vs original => empty diff
2140    let diff = repository
2141        .update(cx, |repository, _| {
2142            repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
2143        })
2144        .await
2145        .unwrap()
2146        .unwrap();
2147    assert!(diff.is_empty(), "diff after restore should be empty");
2148}
2149
2150#[gpui::test]
2151async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2152    let fs = FakeFs::new(server_cx.executor());
2153    fs.insert_tree(
2154        path!("/project"),
2155        json!({
2156            "a.txt": "A",
2157            "b.txt": "B",
2158        }),
2159    )
2160    .await;
2161
2162    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
2163    project
2164        .update(cx, |project, cx| {
2165            project.find_or_create_worktree(path!("/project"), true, cx)
2166        })
2167        .await
2168        .unwrap();
2169
2170    let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
2171
2172    let input = ReadFileToolInput {
2173        path: "project/b.txt".into(),
2174        start_line: None,
2175        end_line: None,
2176    };
2177    let read_tool = Arc::new(ReadFileTool::new(project, action_log, true));
2178    let (event_stream, _) = ToolCallEventStream::test();
2179
2180    let exists_result = cx.update(|cx| {
2181        read_tool
2182            .clone()
2183            .run(ToolInput::resolved(input), event_stream.clone(), cx)
2184    });
2185    let output = exists_result.await.unwrap();
2186    assert_eq!(output, LanguageModelToolResultContent::Text("B".into()));
2187
2188    let input = ReadFileToolInput {
2189        path: "project/c.txt".into(),
2190        start_line: None,
2191        end_line: None,
2192    };
2193    let does_not_exist_result =
2194        cx.update(|cx| read_tool.run(ToolInput::resolved(input), event_stream, cx));
2195    does_not_exist_result.await.unwrap_err();
2196}
2197
2198#[gpui::test]
2199async fn test_remote_external_agent_server(
2200    cx: &mut TestAppContext,
2201    server_cx: &mut TestAppContext,
2202) {
2203    let fs = FakeFs::new(server_cx.executor());
2204    fs.insert_tree(path!("/project"), json!({})).await;
2205
2206    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
2207    project
2208        .update(cx, |project, cx| {
2209            project.find_or_create_worktree(path!("/project"), true, cx)
2210        })
2211        .await
2212        .unwrap();
2213    let names = project.update(cx, |project, cx| {
2214        project
2215            .agent_server_store()
2216            .read(cx)
2217            .external_agents()
2218            .map(|name| name.to_string())
2219            .collect::<Vec<_>>()
2220    });
2221    pretty_assertions::assert_eq!(names, Vec::<String>::new());
2222    server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
2223        settings_store
2224            .set_server_settings(
2225                &json!({
2226                    "agent_servers": {
2227                        "foo": {
2228                            "type": "custom",
2229                            "command": "foo-cli",
2230                            "args": ["--flag"],
2231                            "env": {
2232                                "VAR": "val"
2233                            }
2234                        }
2235                    }
2236                })
2237                .to_string(),
2238                cx,
2239            )
2240            .unwrap();
2241    });
2242    server_cx.run_until_parked();
2243    cx.run_until_parked();
2244    let names = project.update(cx, |project, cx| {
2245        project
2246            .agent_server_store()
2247            .read(cx)
2248            .external_agents()
2249            .map(|name| name.to_string())
2250            .collect::<Vec<_>>()
2251    });
2252    pretty_assertions::assert_eq!(names, ["foo"]);
2253    let command = project
2254        .update(cx, |project, cx| {
2255            project.agent_server_store().update(cx, |store, cx| {
2256                store
2257                    .get_external_agent(&"foo".into())
2258                    .unwrap()
2259                    .get_command(
2260                        vec![],
2261                        HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]),
2262                        &mut cx.to_async(),
2263                    )
2264            })
2265        })
2266        .await
2267        .unwrap();
2268    assert_eq!(
2269        command,
2270        AgentServerCommand {
2271            path: "foo-cli".into(),
2272            args: vec!["--flag".into()],
2273            env: Some(HashMap::from_iter([
2274                ("NO_BROWSER".into(), "1".into()),
2275                ("VAR".into(), "val".into()),
2276                ("OTHER_VAR".into(), "other-val".into())
2277            ]))
2278        }
2279    );
2280}
2281
2282pub async fn init_test(
2283    server_fs: &Arc<FakeFs>,
2284    cx: &mut TestAppContext,
2285    server_cx: &mut TestAppContext,
2286) -> (Entity<Project>, Entity<HeadlessProject>) {
2287    let server_fs = server_fs.clone();
2288    cx.update(|cx| {
2289        release_channel::init(semver::Version::new(0, 0, 0), cx);
2290    });
2291    server_cx.update(|cx| {
2292        release_channel::init(semver::Version::new(0, 0, 0), cx);
2293    });
2294    init_logger();
2295
2296    let (opts, ssh_server_client, _) = RemoteClient::fake_server(cx, server_cx);
2297    let http_client = Arc::new(BlockedHttpClient);
2298    let node_runtime = NodeRuntime::unavailable();
2299    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
2300    let proxy = Arc::new(ExtensionHostProxy::new());
2301    server_cx.update(HeadlessProject::init);
2302    let headless = server_cx.new(|cx| {
2303        HeadlessProject::new(
2304            crate::HeadlessAppState {
2305                session: ssh_server_client,
2306                fs: server_fs.clone(),
2307                http_client,
2308                node_runtime,
2309                languages,
2310                extension_host_proxy: proxy,
2311                startup_time: std::time::Instant::now(),
2312            },
2313            false,
2314            cx,
2315        )
2316    });
2317
2318    let ssh = RemoteClient::connect_mock(opts, cx).await;
2319    let project = build_project(ssh, cx);
2320    project
2321        .update(cx, {
2322            let headless = headless.clone();
2323            |_, cx| cx.on_release(|_, _| drop(headless))
2324        })
2325        .detach();
2326    (project, headless)
2327}
2328
2329fn init_logger() {
2330    zlog::init_test();
2331}
2332
2333fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
2334    cx.update(|cx| {
2335        if !cx.has_global::<SettingsStore>() {
2336            let settings_store = SettingsStore::test(cx);
2337            cx.set_global(settings_store);
2338        }
2339    });
2340
2341    let client = cx.update(|cx| {
2342        Client::new(
2343            Arc::new(FakeSystemClock::new()),
2344            FakeHttpClient::with_404_response(),
2345            cx,
2346        )
2347    });
2348
2349    let node = NodeRuntime::unavailable();
2350    let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
2351    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
2352    let fs = FakeFs::new(cx.executor());
2353
2354    cx.update(|cx| {
2355        Project::init(&client, cx);
2356    });
2357
2358    cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, false, cx))
2359}