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        },
1570    )
1571    .await;
1572
1573    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1574
1575    // Main repo: root_repo_common_dir should be the .git directory itself.
1576    let (worktree_main, _) = project
1577        .update(cx, |project, cx| {
1578            project.find_or_create_worktree("/code/main_repo", true, cx)
1579        })
1580        .await
1581        .unwrap();
1582    cx.executor().run_until_parked();
1583
1584    let common_dir = worktree_main.read_with(cx, |worktree, _| {
1585        worktree.snapshot().root_repo_common_dir().cloned()
1586    });
1587    assert_eq!(
1588        common_dir.as_deref(),
1589        Some(Path::new("/code/main_repo/.git")),
1590    );
1591
1592    // Linked worktree: root_repo_common_dir should point to the main repo's .git.
1593    let (worktree_linked, _) = project
1594        .update(cx, |project, cx| {
1595            project.find_or_create_worktree("/code/linked_worktree", true, cx)
1596        })
1597        .await
1598        .unwrap();
1599    cx.executor().run_until_parked();
1600
1601    let common_dir = worktree_linked.read_with(cx, |worktree, _| {
1602        worktree.snapshot().root_repo_common_dir().cloned()
1603    });
1604    assert_eq!(
1605        common_dir.as_deref(),
1606        Some(Path::new("/code/main_repo/.git")),
1607    );
1608
1609    // No git repo: root_repo_common_dir should be None.
1610    let (worktree_no_git, _) = project
1611        .update(cx, |project, cx| {
1612            project.find_or_create_worktree("/code/no_git", true, cx)
1613        })
1614        .await
1615        .unwrap();
1616    cx.executor().run_until_parked();
1617
1618    let common_dir = worktree_no_git.read_with(cx, |worktree, _| {
1619        worktree.snapshot().root_repo_common_dir().cloned()
1620    });
1621    assert_eq!(common_dir, None);
1622}
1623
1624#[gpui::test]
1625async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1626    let text_2 = "
1627        fn one() -> usize {
1628            1
1629        }
1630    "
1631    .unindent();
1632    let text_1 = "
1633        fn one() -> usize {
1634            0
1635        }
1636    "
1637    .unindent();
1638
1639    let fs = FakeFs::new(server_cx.executor());
1640    fs.insert_tree(
1641        "/code",
1642        json!({
1643            "project1": {
1644                ".git": {},
1645                "src": {
1646                    "lib.rs": text_2
1647                },
1648                "README.md": "# project 1",
1649            },
1650        }),
1651    )
1652    .await;
1653    fs.set_index_for_repo(
1654        Path::new("/code/project1/.git"),
1655        &[("src/lib.rs", text_1.clone())],
1656    );
1657    fs.set_head_for_repo(
1658        Path::new("/code/project1/.git"),
1659        &[("src/lib.rs", text_1.clone())],
1660        "deadbeef",
1661    );
1662
1663    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1664    let (worktree, _) = project
1665        .update(cx, |project, cx| {
1666            project.find_or_create_worktree("/code/project1", true, cx)
1667        })
1668        .await
1669        .unwrap();
1670    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1671    cx.executor().run_until_parked();
1672
1673    let buffer = project
1674        .update(cx, |project, cx| {
1675            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1676        })
1677        .await
1678        .unwrap();
1679    let diff = project
1680        .update(cx, |project, cx| {
1681            project.open_uncommitted_diff(buffer.clone(), cx)
1682        })
1683        .await
1684        .unwrap();
1685
1686    diff.read_with(cx, |diff, cx| {
1687        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1688        assert_eq!(
1689            diff.secondary_diff()
1690                .unwrap()
1691                .read(cx)
1692                .base_text_string(cx)
1693                .unwrap(),
1694            text_1
1695        );
1696    });
1697
1698    // stage the current buffer's contents
1699    fs.set_index_for_repo(
1700        Path::new("/code/project1/.git"),
1701        &[("src/lib.rs", text_2.clone())],
1702    );
1703
1704    cx.executor().run_until_parked();
1705    diff.read_with(cx, |diff, cx| {
1706        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1707        assert_eq!(
1708            diff.secondary_diff()
1709                .unwrap()
1710                .read(cx)
1711                .base_text_string(cx)
1712                .unwrap(),
1713            text_2
1714        );
1715    });
1716
1717    // commit the current buffer's contents
1718    fs.set_head_for_repo(
1719        Path::new("/code/project1/.git"),
1720        &[("src/lib.rs", text_2.clone())],
1721        "deadbeef",
1722    );
1723
1724    cx.executor().run_until_parked();
1725    diff.read_with(cx, |diff, cx| {
1726        assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
1727        assert_eq!(
1728            diff.secondary_diff()
1729                .unwrap()
1730                .read(cx)
1731                .base_text_string(cx)
1732                .unwrap(),
1733            text_2
1734        );
1735    });
1736}
1737
1738#[gpui::test]
1739async fn test_remote_git_diffs_when_recv_update_repository_delay(
1740    cx: &mut TestAppContext,
1741    server_cx: &mut TestAppContext,
1742) {
1743    cx.update(|cx| {
1744        let settings_store = SettingsStore::test(cx);
1745        cx.set_global(settings_store);
1746        theme_settings::init(theme::LoadThemes::JustBase, cx);
1747        release_channel::init(semver::Version::new(0, 0, 0), cx);
1748        editor::init(cx);
1749    });
1750
1751    use editor::Editor;
1752    use gpui::VisualContext;
1753    let text_2 = "
1754        fn one() -> usize {
1755            1
1756        }
1757    "
1758    .unindent();
1759    let text_1 = "
1760        fn one() -> usize {
1761            0
1762        }
1763    "
1764    .unindent();
1765
1766    let fs = FakeFs::new(server_cx.executor());
1767    fs.insert_tree(
1768        path!("/code"),
1769        json!({
1770            "project1": {
1771                "src": {
1772                    "lib.rs": text_2
1773                },
1774                "README.md": "# project 1",
1775            },
1776        }),
1777    )
1778    .await;
1779
1780    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1781    let (worktree, _) = project
1782        .update(cx, |project, cx| {
1783            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1784        })
1785        .await
1786        .unwrap();
1787    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1788    let buffer = project
1789        .update(cx, |project, cx| {
1790            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1791        })
1792        .await
1793        .unwrap();
1794    let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
1795
1796    let cx = cx.add_empty_window();
1797    let editor = cx.new_window_entity(|window, cx| {
1798        Editor::for_buffer(buffer, Some(project.clone()), window, cx)
1799    });
1800
1801    // Remote server will send proto::UpdateRepository after the instance of Editor create.
1802    fs.insert_tree(
1803        path!("/code"),
1804        json!({
1805            "project1": {
1806                ".git": {},
1807            },
1808        }),
1809    )
1810    .await;
1811
1812    fs.set_index_for_repo(
1813        Path::new(path!("/code/project1/.git")),
1814        &[("src/lib.rs", text_1.clone())],
1815    );
1816    fs.set_head_for_repo(
1817        Path::new(path!("/code/project1/.git")),
1818        &[("src/lib.rs", text_1.clone())],
1819        "sha",
1820    );
1821
1822    cx.executor().run_until_parked();
1823    let diff = editor
1824        .read_with(cx, |editor, cx| {
1825            editor
1826                .buffer()
1827                .read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
1828        })
1829        .unwrap();
1830
1831    diff.read_with(cx, |diff, cx| {
1832        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1833        assert_eq!(
1834            diff.secondary_diff()
1835                .unwrap()
1836                .read(cx)
1837                .base_text_string(cx)
1838                .unwrap(),
1839            text_1
1840        );
1841    });
1842
1843    // stage the current buffer's contents
1844    fs.set_index_for_repo(
1845        Path::new(path!("/code/project1/.git")),
1846        &[("src/lib.rs", text_2.clone())],
1847    );
1848
1849    cx.executor().run_until_parked();
1850    diff.read_with(cx, |diff, cx| {
1851        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1852        assert_eq!(
1853            diff.secondary_diff()
1854                .unwrap()
1855                .read(cx)
1856                .base_text_string(cx)
1857                .unwrap(),
1858            text_2
1859        );
1860    });
1861
1862    // commit the current buffer's contents
1863    fs.set_head_for_repo(
1864        Path::new(path!("/code/project1/.git")),
1865        &[("src/lib.rs", text_2.clone())],
1866        "sha",
1867    );
1868
1869    cx.executor().run_until_parked();
1870    diff.read_with(cx, |diff, cx| {
1871        assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
1872        assert_eq!(
1873            diff.secondary_diff()
1874                .unwrap()
1875                .read(cx)
1876                .base_text_string(cx)
1877                .unwrap(),
1878            text_2
1879        );
1880    });
1881}
1882
1883#[gpui::test]
1884async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1885    let fs = FakeFs::new(server_cx.executor());
1886    fs.insert_tree(
1887        path!("/code"),
1888        json!({
1889            "project1": {
1890                ".git": {},
1891                "README.md": "# project 1",
1892            },
1893        }),
1894    )
1895    .await;
1896
1897    let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1898    let branches = ["main", "dev", "feature-1"];
1899    let branches_set = branches
1900        .iter()
1901        .map(ToString::to_string)
1902        .collect::<HashSet<_>>();
1903    fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1904
1905    let (_worktree, _) = project
1906        .update(cx, |project, cx| {
1907            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1908        })
1909        .await
1910        .unwrap();
1911    // Give the worktree a bit of time to index the file system
1912    cx.run_until_parked();
1913
1914    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1915
1916    let remote_branches = repository
1917        .update(cx, |repository, _| repository.branches())
1918        .await
1919        .unwrap()
1920        .unwrap();
1921
1922    let new_branch = branches[2];
1923
1924    let remote_branches = remote_branches
1925        .into_iter()
1926        .map(|branch| branch.name().to_string())
1927        .collect::<HashSet<_>>();
1928
1929    assert_eq!(&remote_branches, &branches_set);
1930
1931    cx.update(|cx| {
1932        repository.update(cx, |repository, _cx| {
1933            repository.change_branch(new_branch.to_string())
1934        })
1935    })
1936    .await
1937    .unwrap()
1938    .unwrap();
1939
1940    cx.run_until_parked();
1941
1942    let server_branch = server_cx.update(|cx| {
1943        headless_project.update(cx, |headless_project, cx| {
1944            headless_project.git_store.update(cx, |git_store, cx| {
1945                git_store
1946                    .repositories()
1947                    .values()
1948                    .next()
1949                    .unwrap()
1950                    .read(cx)
1951                    .branch
1952                    .as_ref()
1953                    .unwrap()
1954                    .clone()
1955            })
1956        })
1957    });
1958
1959    assert_eq!(server_branch.name(), branches[2]);
1960
1961    // Also try creating a new branch
1962    cx.update(|cx| {
1963        repository.update(cx, |repo, _cx| {
1964            repo.create_branch("totally-new-branch".to_string(), None)
1965        })
1966    })
1967    .await
1968    .unwrap()
1969    .unwrap();
1970
1971    cx.update(|cx| {
1972        repository.update(cx, |repo, _cx| {
1973            repo.change_branch("totally-new-branch".to_string())
1974        })
1975    })
1976    .await
1977    .unwrap()
1978    .unwrap();
1979
1980    cx.run_until_parked();
1981
1982    let server_branch = server_cx.update(|cx| {
1983        headless_project.update(cx, |headless_project, cx| {
1984            headless_project.git_store.update(cx, |git_store, cx| {
1985                git_store
1986                    .repositories()
1987                    .values()
1988                    .next()
1989                    .unwrap()
1990                    .read(cx)
1991                    .branch
1992                    .as_ref()
1993                    .unwrap()
1994                    .clone()
1995            })
1996        })
1997    });
1998
1999    assert_eq!(server_branch.name(), "totally-new-branch");
2000}
2001
2002#[gpui::test]
2003async fn test_remote_git_checkpoints(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2004    let fs = FakeFs::new(server_cx.executor());
2005    fs.insert_tree(
2006        path!("/code"),
2007        json!({
2008            "project1": {
2009                ".git": {},
2010                "file.txt": "original content",
2011            },
2012        }),
2013    )
2014    .await;
2015
2016    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2017
2018    let (_worktree, _) = project
2019        .update(cx, |project, cx| {
2020            project.find_or_create_worktree(path!("/code/project1"), true, cx)
2021        })
2022        .await
2023        .unwrap();
2024    cx.run_until_parked();
2025
2026    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
2027
2028    // 1. Create a checkpoint of the original state
2029    let checkpoint_1 = repository
2030        .update(cx, |repository, _| repository.checkpoint())
2031        .await
2032        .unwrap()
2033        .unwrap();
2034
2035    // 2. Modify a file on the server-side fs
2036    fs.write(
2037        Path::new(path!("/code/project1/file.txt")),
2038        b"modified content",
2039    )
2040    .await
2041    .unwrap();
2042
2043    // 3. Create a second checkpoint with the modified state
2044    let checkpoint_2 = repository
2045        .update(cx, |repository, _| repository.checkpoint())
2046        .await
2047        .unwrap()
2048        .unwrap();
2049
2050    // 4. compare_checkpoints: same checkpoint with itself => equal
2051    let equal = repository
2052        .update(cx, |repository, _| {
2053            repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
2054        })
2055        .await
2056        .unwrap()
2057        .unwrap();
2058    assert!(equal, "a checkpoint compared with itself should be equal");
2059
2060    // 5. compare_checkpoints: different states => not equal
2061    let equal = repository
2062        .update(cx, |repository, _| {
2063            repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
2064        })
2065        .await
2066        .unwrap()
2067        .unwrap();
2068    assert!(
2069        !equal,
2070        "checkpoints of different states should not be equal"
2071    );
2072
2073    // 6. diff_checkpoints: same checkpoint => empty diff
2074    let diff = repository
2075        .update(cx, |repository, _| {
2076            repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
2077        })
2078        .await
2079        .unwrap()
2080        .unwrap();
2081    assert!(
2082        diff.is_empty(),
2083        "diff of identical checkpoints should be empty"
2084    );
2085
2086    // 7. diff_checkpoints: different checkpoints => non-empty diff mentioning the changed file
2087    let diff = repository
2088        .update(cx, |repository, _| {
2089            repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
2090        })
2091        .await
2092        .unwrap()
2093        .unwrap();
2094    assert!(
2095        !diff.is_empty(),
2096        "diff of different checkpoints should be non-empty"
2097    );
2098    assert!(
2099        diff.contains("file.txt"),
2100        "diff should mention the changed file"
2101    );
2102    assert!(
2103        diff.contains("original content"),
2104        "diff should contain removed content"
2105    );
2106    assert!(
2107        diff.contains("modified content"),
2108        "diff should contain added content"
2109    );
2110
2111    // 8. restore_checkpoint: restore to original state
2112    repository
2113        .update(cx, |repository, _| {
2114            repository.restore_checkpoint(checkpoint_1.clone())
2115        })
2116        .await
2117        .unwrap()
2118        .unwrap();
2119    cx.run_until_parked();
2120
2121    // 9. Create a checkpoint after restore
2122    let checkpoint_3 = repository
2123        .update(cx, |repository, _| repository.checkpoint())
2124        .await
2125        .unwrap()
2126        .unwrap();
2127
2128    // 10. compare_checkpoints: restored state matches original
2129    let equal = repository
2130        .update(cx, |repository, _| {
2131            repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
2132        })
2133        .await
2134        .unwrap()
2135        .unwrap();
2136    assert!(equal, "restored state should match original checkpoint");
2137
2138    // 11. diff_checkpoints: restored state vs original => empty diff
2139    let diff = repository
2140        .update(cx, |repository, _| {
2141            repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
2142        })
2143        .await
2144        .unwrap()
2145        .unwrap();
2146    assert!(diff.is_empty(), "diff after restore should be empty");
2147}
2148
2149#[gpui::test]
2150async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2151    let fs = FakeFs::new(server_cx.executor());
2152    fs.insert_tree(
2153        path!("/project"),
2154        json!({
2155            "a.txt": "A",
2156            "b.txt": "B",
2157        }),
2158    )
2159    .await;
2160
2161    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
2162    project
2163        .update(cx, |project, cx| {
2164            project.find_or_create_worktree(path!("/project"), true, cx)
2165        })
2166        .await
2167        .unwrap();
2168
2169    let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
2170
2171    let input = ReadFileToolInput {
2172        path: "project/b.txt".into(),
2173        start_line: None,
2174        end_line: None,
2175    };
2176    let read_tool = Arc::new(ReadFileTool::new(project, action_log, true));
2177    let (event_stream, _) = ToolCallEventStream::test();
2178
2179    let exists_result = cx.update(|cx| {
2180        read_tool
2181            .clone()
2182            .run(ToolInput::resolved(input), event_stream.clone(), cx)
2183    });
2184    let output = exists_result.await.unwrap();
2185    assert_eq!(output, LanguageModelToolResultContent::Text("B".into()));
2186
2187    let input = ReadFileToolInput {
2188        path: "project/c.txt".into(),
2189        start_line: None,
2190        end_line: None,
2191    };
2192    let does_not_exist_result =
2193        cx.update(|cx| read_tool.run(ToolInput::resolved(input), event_stream, cx));
2194    does_not_exist_result.await.unwrap_err();
2195}
2196
2197#[gpui::test]
2198async fn test_remote_external_agent_server(
2199    cx: &mut TestAppContext,
2200    server_cx: &mut TestAppContext,
2201) {
2202    let fs = FakeFs::new(server_cx.executor());
2203    fs.insert_tree(path!("/project"), json!({})).await;
2204
2205    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
2206    project
2207        .update(cx, |project, cx| {
2208            project.find_or_create_worktree(path!("/project"), true, cx)
2209        })
2210        .await
2211        .unwrap();
2212    let names = project.update(cx, |project, cx| {
2213        project
2214            .agent_server_store()
2215            .read(cx)
2216            .external_agents()
2217            .map(|name| name.to_string())
2218            .collect::<Vec<_>>()
2219    });
2220    pretty_assertions::assert_eq!(names, Vec::<String>::new());
2221    server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
2222        settings_store
2223            .set_server_settings(
2224                &json!({
2225                    "agent_servers": {
2226                        "foo": {
2227                            "type": "custom",
2228                            "command": "foo-cli",
2229                            "args": ["--flag"],
2230                            "env": {
2231                                "VAR": "val"
2232                            }
2233                        }
2234                    }
2235                })
2236                .to_string(),
2237                cx,
2238            )
2239            .unwrap();
2240    });
2241    server_cx.run_until_parked();
2242    cx.run_until_parked();
2243    let names = project.update(cx, |project, cx| {
2244        project
2245            .agent_server_store()
2246            .read(cx)
2247            .external_agents()
2248            .map(|name| name.to_string())
2249            .collect::<Vec<_>>()
2250    });
2251    pretty_assertions::assert_eq!(names, ["foo"]);
2252    let command = project
2253        .update(cx, |project, cx| {
2254            project.agent_server_store().update(cx, |store, cx| {
2255                store
2256                    .get_external_agent(&"foo".into())
2257                    .unwrap()
2258                    .get_command(
2259                        vec![],
2260                        HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]),
2261                        &mut cx.to_async(),
2262                    )
2263            })
2264        })
2265        .await
2266        .unwrap();
2267    assert_eq!(
2268        command,
2269        AgentServerCommand {
2270            path: "foo-cli".into(),
2271            args: vec!["--flag".into()],
2272            env: Some(HashMap::from_iter([
2273                ("NO_BROWSER".into(), "1".into()),
2274                ("VAR".into(), "val".into()),
2275                ("OTHER_VAR".into(), "other-val".into())
2276            ]))
2277        }
2278    );
2279}
2280
2281pub async fn init_test(
2282    server_fs: &Arc<FakeFs>,
2283    cx: &mut TestAppContext,
2284    server_cx: &mut TestAppContext,
2285) -> (Entity<Project>, Entity<HeadlessProject>) {
2286    let server_fs = server_fs.clone();
2287    cx.update(|cx| {
2288        release_channel::init(semver::Version::new(0, 0, 0), cx);
2289    });
2290    server_cx.update(|cx| {
2291        release_channel::init(semver::Version::new(0, 0, 0), cx);
2292    });
2293    init_logger();
2294
2295    let (opts, ssh_server_client, _) = RemoteClient::fake_server(cx, server_cx);
2296    let http_client = Arc::new(BlockedHttpClient);
2297    let node_runtime = NodeRuntime::unavailable();
2298    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
2299    let proxy = Arc::new(ExtensionHostProxy::new());
2300    server_cx.update(HeadlessProject::init);
2301    let headless = server_cx.new(|cx| {
2302        HeadlessProject::new(
2303            crate::HeadlessAppState {
2304                session: ssh_server_client,
2305                fs: server_fs.clone(),
2306                http_client,
2307                node_runtime,
2308                languages,
2309                extension_host_proxy: proxy,
2310                startup_time: std::time::Instant::now(),
2311            },
2312            false,
2313            cx,
2314        )
2315    });
2316
2317    let ssh = RemoteClient::connect_mock(opts, cx).await;
2318    let project = build_project(ssh, cx);
2319    project
2320        .update(cx, {
2321            let headless = headless.clone();
2322            |_, cx| cx.on_release(|_, _| drop(headless))
2323        })
2324        .detach();
2325    (project, headless)
2326}
2327
2328fn init_logger() {
2329    zlog::init_test();
2330}
2331
2332fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
2333    cx.update(|cx| {
2334        if !cx.has_global::<SettingsStore>() {
2335            let settings_store = SettingsStore::test(cx);
2336            cx.set_global(settings_store);
2337        }
2338    });
2339
2340    let client = cx.update(|cx| {
2341        Client::new(
2342            Arc::new(FakeSystemClock::new()),
2343            FakeHttpClient::with_404_response(),
2344            cx,
2345        )
2346    });
2347
2348    let node = NodeRuntime::unavailable();
2349    let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
2350    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
2351    let fs = FakeFs::new(cx.executor());
2352
2353    cx.update(|cx| {
2354        Project::init(&client, cx);
2355    });
2356
2357    cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, false, cx))
2358}