remote_editing_collaboration_tests.rs

   1use crate::TestServer;
   2use call::ActiveCall;
   3use collections::{HashMap, HashSet};
   4
   5use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
   6use debugger_ui::debugger_panel::DebugPanel;
   7use editor::{Editor, EditorMode, MultiBuffer};
   8use extension::ExtensionHostProxy;
   9use fs::{FakeFs, Fs as _, RemoveOptions};
  10use futures::StreamExt as _;
  11use gpui::{
  12    AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext as _,
  13};
  14use http_client::BlockedHttpClient;
  15use language::{
  16    FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
  17    language_settings::{Formatter, FormatterList, LanguageSettings},
  18    rust_lang, tree_sitter_typescript,
  19};
  20use node_runtime::NodeRuntime;
  21use project::{
  22    ProjectPath,
  23    debugger::session::ThreadId,
  24    lsp_store::{FormatTrigger, LspFormatTarget},
  25    trusted_worktrees::{PathTrust, TrustedWorktrees},
  26};
  27use remote::RemoteClient;
  28use remote_server::{HeadlessAppState, HeadlessProject};
  29use rpc::proto;
  30use serde_json::json;
  31use settings::{
  32    InlayHintSettingsContent, LanguageServerFormatterSpecifier, PrettierSettingsContent,
  33    SettingsStore,
  34};
  35use std::{
  36    path::{Path, PathBuf},
  37    sync::{
  38        Arc,
  39        atomic::{AtomicUsize, Ordering},
  40    },
  41    time::Duration,
  42};
  43use task::TcpArgumentsTemplate;
  44use util::{path, rel_path::rel_path};
  45
  46#[gpui::test(iterations = 10)]
  47async fn test_sharing_an_ssh_remote_project(
  48    cx_a: &mut TestAppContext,
  49    cx_b: &mut TestAppContext,
  50    server_cx: &mut TestAppContext,
  51) {
  52    let executor = cx_a.executor();
  53    cx_a.update(|cx| {
  54        release_channel::init(semver::Version::new(0, 0, 0), cx);
  55    });
  56    server_cx.update(|cx| {
  57        release_channel::init(semver::Version::new(0, 0, 0), cx);
  58    });
  59    let mut server = TestServer::start(executor.clone()).await;
  60    let client_a = server.create_client(cx_a, "user_a").await;
  61    let client_b = server.create_client(cx_b, "user_b").await;
  62    server
  63        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
  64        .await;
  65
  66    // Set up project on remote FS
  67    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
  68    let remote_fs = FakeFs::new(server_cx.executor());
  69    remote_fs
  70        .insert_tree(
  71            path!("/code"),
  72            json!({
  73                "project1": {
  74                    ".zed": {
  75                        "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
  76                    },
  77                    "README.md": "# project 1",
  78                    "src": {
  79                        "lib.rs": "fn one() -> usize { 1 }"
  80                    }
  81                },
  82                "project2": {
  83                    "README.md": "# project 2",
  84                },
  85            }),
  86        )
  87        .await;
  88
  89    // User A connects to the remote project via SSH.
  90    server_cx.update(HeadlessProject::init);
  91    let remote_http_client = Arc::new(BlockedHttpClient);
  92    let node = NodeRuntime::unavailable();
  93    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
  94    languages.add(rust_lang());
  95    let _headless_project = server_cx.new(|cx| {
  96        HeadlessProject::new(
  97            HeadlessAppState {
  98                session: server_ssh,
  99                fs: remote_fs.clone(),
 100                http_client: remote_http_client,
 101                node_runtime: node,
 102                languages,
 103                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 104                startup_time: std::time::Instant::now(),
 105            },
 106            false,
 107            cx,
 108        )
 109    });
 110
 111    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 112    let (project_a, worktree_id) = client_a
 113        .build_ssh_project(path!("/code/project1"), client_ssh, false, cx_a)
 114        .await;
 115
 116    // While the SSH worktree is being scanned, user A shares the remote project.
 117    let active_call_a = cx_a.read(ActiveCall::global);
 118    let project_id = active_call_a
 119        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 120        .await
 121        .unwrap();
 122
 123    // User B joins the project.
 124    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 125    project_b.update(cx_b, |project, _| project.languages().add(rust_lang()));
 126    let worktree_b = project_b
 127        .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
 128        .unwrap();
 129
 130    let worktree_a = project_a
 131        .update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx))
 132        .unwrap();
 133
 134    executor.run_until_parked();
 135
 136    worktree_a.update(cx_a, |worktree, _cx| {
 137        assert_eq!(
 138            worktree.paths().collect::<Vec<_>>(),
 139            vec![
 140                rel_path(".zed"),
 141                rel_path(".zed/settings.json"),
 142                rel_path("README.md"),
 143                rel_path("src"),
 144                rel_path("src/lib.rs"),
 145            ]
 146        );
 147    });
 148
 149    worktree_b.update(cx_b, |worktree, _cx| {
 150        assert_eq!(
 151            worktree.paths().collect::<Vec<_>>(),
 152            vec![
 153                rel_path(".zed"),
 154                rel_path(".zed/settings.json"),
 155                rel_path("README.md"),
 156                rel_path("src"),
 157                rel_path("src/lib.rs"),
 158            ]
 159        );
 160    });
 161
 162    // User B can open buffers in the remote project.
 163    let buffer_b = project_b
 164        .update(cx_b, |project, cx| {
 165            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
 166        })
 167        .await
 168        .unwrap();
 169    buffer_b.update(cx_b, |buffer, cx| {
 170        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
 171        let ix = buffer.text().find('1').unwrap();
 172        buffer.edit([(ix..ix + 1, "100")], None, cx);
 173    });
 174
 175    executor.run_until_parked();
 176
 177    cx_b.read(|cx| {
 178        assert_eq!(
 179            LanguageSettings::for_buffer(buffer_b.read(cx), cx).language_servers,
 180            ["override-rust-analyzer".to_string()]
 181        )
 182    });
 183
 184    project_b
 185        .update(cx_b, |project, cx| {
 186            project.save_buffer_as(
 187                buffer_b.clone(),
 188                ProjectPath {
 189                    worktree_id: worktree_id.to_owned(),
 190                    path: rel_path("src/renamed.rs").into(),
 191                },
 192                cx,
 193            )
 194        })
 195        .await
 196        .unwrap();
 197    assert_eq!(
 198        remote_fs
 199            .load(path!("/code/project1/src/renamed.rs").as_ref())
 200            .await
 201            .unwrap(),
 202        "fn one() -> usize { 100 }"
 203    );
 204    cx_b.run_until_parked();
 205    cx_b.update(|cx| {
 206        assert_eq!(
 207            buffer_b.read(cx).file().unwrap().path().as_ref(),
 208            rel_path("src/renamed.rs")
 209        );
 210    });
 211}
 212
 213#[gpui::test]
 214async fn test_ssh_collaboration_git_branches(
 215    executor: BackgroundExecutor,
 216    cx_a: &mut TestAppContext,
 217    cx_b: &mut TestAppContext,
 218    server_cx: &mut TestAppContext,
 219) {
 220    cx_a.set_name("a");
 221    cx_b.set_name("b");
 222    server_cx.set_name("server");
 223
 224    cx_a.update(|cx| {
 225        release_channel::init(semver::Version::new(0, 0, 0), cx);
 226    });
 227    server_cx.update(|cx| {
 228        release_channel::init(semver::Version::new(0, 0, 0), cx);
 229    });
 230
 231    let mut server = TestServer::start(executor.clone()).await;
 232    let client_a = server.create_client(cx_a, "user_a").await;
 233    let client_b = server.create_client(cx_b, "user_b").await;
 234    server
 235        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 236        .await;
 237
 238    // Set up project on remote FS
 239    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 240    let remote_fs = FakeFs::new(server_cx.executor());
 241    remote_fs
 242        .insert_tree("/project", serde_json::json!({ ".git":{} }))
 243        .await;
 244
 245    let branches = ["main", "dev", "feature-1"];
 246    let branches_set = branches
 247        .iter()
 248        .map(ToString::to_string)
 249        .collect::<HashSet<_>>();
 250    remote_fs.insert_branches(Path::new("/project/.git"), &branches);
 251
 252    // User A connects to the remote project via SSH.
 253    server_cx.update(HeadlessProject::init);
 254    let remote_http_client = Arc::new(BlockedHttpClient);
 255    let node = NodeRuntime::unavailable();
 256    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 257    let headless_project = server_cx.new(|cx| {
 258        HeadlessProject::new(
 259            HeadlessAppState {
 260                session: server_ssh,
 261                fs: remote_fs.clone(),
 262                http_client: remote_http_client,
 263                node_runtime: node,
 264                languages,
 265                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 266                startup_time: std::time::Instant::now(),
 267            },
 268            false,
 269            cx,
 270        )
 271    });
 272
 273    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 274    let (project_a, _) = client_a
 275        .build_ssh_project("/project", client_ssh, false, cx_a)
 276        .await;
 277
 278    // While the SSH worktree is being scanned, user A shares the remote project.
 279    let active_call_a = cx_a.read(ActiveCall::global);
 280    let project_id = active_call_a
 281        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 282        .await
 283        .unwrap();
 284
 285    // User B joins the project.
 286    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 287
 288    // Give client A sometime to see that B has joined, and that the headless server
 289    // has some git repositories
 290    executor.run_until_parked();
 291
 292    let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
 293
 294    let branches_b = cx_b
 295        .update(|cx| repo_b.update(cx, |repo_b, _cx| repo_b.branches()))
 296        .await
 297        .unwrap()
 298        .unwrap();
 299
 300    let new_branch = branches[2];
 301
 302    let branches_b = branches_b
 303        .into_iter()
 304        .map(|branch| branch.name().to_string())
 305        .collect::<HashSet<_>>();
 306
 307    assert_eq!(&branches_b, &branches_set);
 308
 309    cx_b.update(|cx| {
 310        repo_b.update(cx, |repo_b, _cx| {
 311            repo_b.change_branch(new_branch.to_string())
 312        })
 313    })
 314    .await
 315    .unwrap()
 316    .unwrap();
 317
 318    executor.run_until_parked();
 319
 320    let server_branch = server_cx.update(|cx| {
 321        headless_project.update(cx, |headless_project, cx| {
 322            headless_project.git_store.update(cx, |git_store, cx| {
 323                git_store
 324                    .repositories()
 325                    .values()
 326                    .next()
 327                    .unwrap()
 328                    .read(cx)
 329                    .branch
 330                    .as_ref()
 331                    .unwrap()
 332                    .clone()
 333            })
 334        })
 335    });
 336
 337    assert_eq!(server_branch.name(), branches[2]);
 338
 339    // Also try creating a new branch
 340    cx_b.update(|cx| {
 341        repo_b.update(cx, |repo_b, _cx| {
 342            repo_b.create_branch("totally-new-branch".to_string(), None)
 343        })
 344    })
 345    .await
 346    .unwrap()
 347    .unwrap();
 348
 349    cx_b.update(|cx| {
 350        repo_b.update(cx, |repo_b, _cx| {
 351            repo_b.change_branch("totally-new-branch".to_string())
 352        })
 353    })
 354    .await
 355    .unwrap()
 356    .unwrap();
 357
 358    executor.run_until_parked();
 359
 360    let server_branch = server_cx.update(|cx| {
 361        headless_project.update(cx, |headless_project, cx| {
 362            headless_project.git_store.update(cx, |git_store, cx| {
 363                git_store
 364                    .repositories()
 365                    .values()
 366                    .next()
 367                    .unwrap()
 368                    .read(cx)
 369                    .branch
 370                    .as_ref()
 371                    .unwrap()
 372                    .clone()
 373            })
 374        })
 375    });
 376
 377    assert_eq!(server_branch.name(), "totally-new-branch");
 378
 379    // Remove the git repository and check that all participants get the update.
 380    remote_fs
 381        .remove_dir("/project/.git".as_ref(), RemoveOptions::default())
 382        .await
 383        .unwrap();
 384    executor.run_until_parked();
 385
 386    project_a.update(cx_a, |project, cx| {
 387        pretty_assertions::assert_eq!(
 388            project.git_store().read(cx).repo_snapshots(cx),
 389            HashMap::default()
 390        );
 391    });
 392    project_b.update(cx_b, |project, cx| {
 393        pretty_assertions::assert_eq!(
 394            project.git_store().read(cx).repo_snapshots(cx),
 395            HashMap::default()
 396        );
 397    });
 398}
 399
 400#[gpui::test]
 401async fn test_ssh_collaboration_git_worktrees(
 402    executor: BackgroundExecutor,
 403    cx_a: &mut TestAppContext,
 404    cx_b: &mut TestAppContext,
 405    server_cx: &mut TestAppContext,
 406) {
 407    cx_a.set_name("a");
 408    cx_b.set_name("b");
 409    server_cx.set_name("server");
 410
 411    cx_a.update(|cx| {
 412        release_channel::init(semver::Version::new(0, 0, 0), cx);
 413    });
 414    server_cx.update(|cx| {
 415        release_channel::init(semver::Version::new(0, 0, 0), cx);
 416    });
 417
 418    let mut server = TestServer::start(executor.clone()).await;
 419    let client_a = server.create_client(cx_a, "user_a").await;
 420    let client_b = server.create_client(cx_b, "user_b").await;
 421    server
 422        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 423        .await;
 424
 425    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 426    let remote_fs = FakeFs::new(server_cx.executor());
 427    remote_fs
 428        .insert_tree("/project", json!({ ".git": {}, "file.txt": "content" }))
 429        .await;
 430
 431    server_cx.update(HeadlessProject::init);
 432    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 433    let headless_project = server_cx.new(|cx| {
 434        HeadlessProject::new(
 435            HeadlessAppState {
 436                session: server_ssh,
 437                fs: remote_fs.clone(),
 438                http_client: Arc::new(BlockedHttpClient),
 439                node_runtime: NodeRuntime::unavailable(),
 440                languages,
 441                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 442                startup_time: std::time::Instant::now(),
 443            },
 444            false,
 445            cx,
 446        )
 447    });
 448
 449    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 450    let (project_a, _) = client_a
 451        .build_ssh_project("/project", client_ssh, false, cx_a)
 452        .await;
 453
 454    let active_call_a = cx_a.read(ActiveCall::global);
 455    let project_id = active_call_a
 456        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 457        .await
 458        .unwrap();
 459    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 460
 461    executor.run_until_parked();
 462
 463    let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
 464
 465    let worktrees = cx_b
 466        .update(|cx| repo_b.update(cx, |repo, _| repo.worktrees()))
 467        .await
 468        .unwrap()
 469        .unwrap();
 470    assert_eq!(worktrees.len(), 1);
 471
 472    let worktree_directory = PathBuf::from("/worktrees");
 473    cx_b.update(|cx| {
 474        repo_b.update(cx, |repo, _| {
 475            repo.create_worktree(
 476                git::repository::CreateWorktreeTarget::NewBranch {
 477                    branch_name: "feature-branch".to_string(),
 478                    base_sha: Some("abc123".to_string()),
 479                },
 480                worktree_directory.join("feature-branch"),
 481            )
 482        })
 483    })
 484    .await
 485    .unwrap()
 486    .unwrap();
 487
 488    executor.run_until_parked();
 489
 490    let worktrees = cx_b
 491        .update(|cx| repo_b.update(cx, |repo, _| repo.worktrees()))
 492        .await
 493        .unwrap()
 494        .unwrap();
 495    assert_eq!(worktrees.len(), 2);
 496    assert_eq!(worktrees[1].path, worktree_directory.join("feature-branch"));
 497    assert_eq!(
 498        worktrees[1].ref_name,
 499        Some("refs/heads/feature-branch".into())
 500    );
 501    assert_eq!(worktrees[1].sha.as_ref(), "abc123");
 502
 503    let server_worktrees = {
 504        let server_repo = server_cx.update(|cx| {
 505            headless_project.update(cx, |headless_project, cx| {
 506                headless_project
 507                    .git_store
 508                    .read(cx)
 509                    .repositories()
 510                    .values()
 511                    .next()
 512                    .unwrap()
 513                    .clone()
 514            })
 515        });
 516        server_cx
 517            .update(|cx| server_repo.update(cx, |repo, _| repo.worktrees()))
 518            .await
 519            .unwrap()
 520            .unwrap()
 521    };
 522    assert_eq!(server_worktrees.len(), 2);
 523    assert_eq!(
 524        server_worktrees[1].path,
 525        worktree_directory.join("feature-branch")
 526    );
 527
 528    // Host (client A) renames the worktree via SSH
 529    let repo_a = cx_a.update(|cx| {
 530        project_a
 531            .read(cx)
 532            .repositories(cx)
 533            .values()
 534            .next()
 535            .unwrap()
 536            .clone()
 537    });
 538    cx_a.update(|cx| {
 539        repo_a.update(cx, |repository, _| {
 540            repository.rename_worktree(
 541                PathBuf::from("/worktrees/feature-branch"),
 542                PathBuf::from("/worktrees/renamed-branch"),
 543            )
 544        })
 545    })
 546    .await
 547    .unwrap()
 548    .unwrap();
 549
 550    executor.run_until_parked();
 551
 552    let host_worktrees = cx_a
 553        .update(|cx| repo_a.update(cx, |repository, _| repository.worktrees()))
 554        .await
 555        .unwrap()
 556        .unwrap();
 557    assert_eq!(
 558        host_worktrees.len(),
 559        2,
 560        "Host should still have 2 worktrees after rename"
 561    );
 562    assert_eq!(
 563        host_worktrees[1].path,
 564        PathBuf::from("/worktrees/renamed-branch")
 565    );
 566
 567    let server_worktrees = {
 568        let server_repo = server_cx.update(|cx| {
 569            headless_project.update(cx, |headless_project, cx| {
 570                headless_project
 571                    .git_store
 572                    .read(cx)
 573                    .repositories()
 574                    .values()
 575                    .next()
 576                    .unwrap()
 577                    .clone()
 578            })
 579        });
 580        server_cx
 581            .update(|cx| server_repo.update(cx, |repo, _| repo.worktrees()))
 582            .await
 583            .unwrap()
 584            .unwrap()
 585    };
 586    assert_eq!(
 587        server_worktrees.len(),
 588        2,
 589        "Server should still have 2 worktrees after rename"
 590    );
 591    assert_eq!(
 592        server_worktrees[1].path,
 593        PathBuf::from("/worktrees/renamed-branch")
 594    );
 595
 596    // Host (client A) removes the renamed worktree via SSH
 597    cx_a.update(|cx| {
 598        repo_a.update(cx, |repository, _| {
 599            repository.remove_worktree(PathBuf::from("/worktrees/renamed-branch"), false)
 600        })
 601    })
 602    .await
 603    .unwrap()
 604    .unwrap();
 605
 606    executor.run_until_parked();
 607
 608    let host_worktrees = cx_a
 609        .update(|cx| repo_a.update(cx, |repository, _| repository.worktrees()))
 610        .await
 611        .unwrap()
 612        .unwrap();
 613    assert_eq!(
 614        host_worktrees.len(),
 615        1,
 616        "Host should only have the main worktree after removal"
 617    );
 618
 619    let server_worktrees = {
 620        let server_repo = server_cx.update(|cx| {
 621            headless_project.update(cx, |headless_project, cx| {
 622                headless_project
 623                    .git_store
 624                    .read(cx)
 625                    .repositories()
 626                    .values()
 627                    .next()
 628                    .unwrap()
 629                    .clone()
 630            })
 631        });
 632        server_cx
 633            .update(|cx| server_repo.update(cx, |repo, _| repo.worktrees()))
 634            .await
 635            .unwrap()
 636            .unwrap()
 637    };
 638    assert_eq!(
 639        server_worktrees.len(),
 640        1,
 641        "Server should only have the main worktree after removal"
 642    );
 643}
 644
 645#[gpui::test]
 646async fn test_ssh_collaboration_formatting_with_prettier(
 647    executor: BackgroundExecutor,
 648    cx_a: &mut TestAppContext,
 649    cx_b: &mut TestAppContext,
 650    server_cx: &mut TestAppContext,
 651) {
 652    cx_a.set_name("a");
 653    cx_b.set_name("b");
 654    server_cx.set_name("server");
 655
 656    cx_a.update(|cx| {
 657        release_channel::init(semver::Version::new(0, 0, 0), cx);
 658    });
 659    server_cx.update(|cx| {
 660        release_channel::init(semver::Version::new(0, 0, 0), cx);
 661    });
 662
 663    let mut server = TestServer::start(executor.clone()).await;
 664    let client_a = server.create_client(cx_a, "user_a").await;
 665    let client_b = server.create_client(cx_b, "user_b").await;
 666    server
 667        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 668        .await;
 669
 670    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 671    let remote_fs = FakeFs::new(server_cx.executor());
 672    let buffer_text = "let one = \"two\"";
 673    let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
 674    remote_fs
 675        .insert_tree(
 676            path!("/project"),
 677            serde_json::json!({ "a.ts": buffer_text }),
 678        )
 679        .await;
 680
 681    let test_plugin = "test_plugin";
 682    let ts_lang = Arc::new(Language::new(
 683        LanguageConfig {
 684            name: "TypeScript".into(),
 685            matcher: LanguageMatcher {
 686                path_suffixes: vec!["ts".to_string()],
 687                ..LanguageMatcher::default()
 688            },
 689            ..LanguageConfig::default()
 690        },
 691        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
 692    ));
 693    client_a.language_registry().add(ts_lang.clone());
 694    client_b.language_registry().add(ts_lang.clone());
 695
 696    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 697    let mut fake_language_servers = languages.register_fake_lsp(
 698        "TypeScript",
 699        FakeLspAdapter {
 700            prettier_plugins: vec![test_plugin],
 701            ..Default::default()
 702        },
 703    );
 704
 705    // User A connects to the remote project via SSH.
 706    server_cx.update(HeadlessProject::init);
 707    let remote_http_client = Arc::new(BlockedHttpClient);
 708    let _headless_project = server_cx.new(|cx| {
 709        HeadlessProject::new(
 710            HeadlessAppState {
 711                session: server_ssh,
 712                fs: remote_fs.clone(),
 713                http_client: remote_http_client,
 714                node_runtime: NodeRuntime::unavailable(),
 715                languages,
 716                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 717                startup_time: std::time::Instant::now(),
 718            },
 719            false,
 720            cx,
 721        )
 722    });
 723
 724    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 725    let (project_a, worktree_id) = client_a
 726        .build_ssh_project(path!("/project"), client_ssh, false, cx_a)
 727        .await;
 728
 729    // While the SSH worktree is being scanned, user A shares the remote project.
 730    let active_call_a = cx_a.read(ActiveCall::global);
 731    let project_id = active_call_a
 732        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 733        .await
 734        .unwrap();
 735
 736    // User B joins the project.
 737    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 738    executor.run_until_parked();
 739
 740    // Opens the buffer and formats it
 741    let (buffer_b, _handle) = project_b
 742        .update(cx_b, |p, cx| {
 743            p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
 744        })
 745        .await
 746        .expect("user B opens buffer for formatting");
 747
 748    cx_a.update(|cx| {
 749        SettingsStore::update_global(cx, |store, cx| {
 750            store.update_user_settings(cx, |file| {
 751                file.project.all_languages.defaults.formatter = Some(FormatterList::default());
 752                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
 753                    allowed: Some(true),
 754                    ..Default::default()
 755                });
 756            });
 757        });
 758    });
 759    cx_b.update(|cx| {
 760        SettingsStore::update_global(cx, |store, cx| {
 761            store.update_user_settings(cx, |file| {
 762                file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
 763                    Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
 764                ));
 765                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
 766                    allowed: Some(true),
 767                    ..Default::default()
 768                });
 769            });
 770        });
 771    });
 772    let fake_language_server = fake_language_servers.next().await.unwrap();
 773    fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
 774        panic!(
 775            "Unexpected: prettier should be preferred since it's enabled and language supports it"
 776        )
 777    });
 778
 779    project_b
 780        .update(cx_b, |project, cx| {
 781            project.format(
 782                HashSet::from_iter([buffer_b.clone()]),
 783                LspFormatTarget::Buffers,
 784                true,
 785                FormatTrigger::Save,
 786                cx,
 787            )
 788        })
 789        .await
 790        .unwrap();
 791
 792    executor.run_until_parked();
 793    assert_eq!(
 794        buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
 795        buffer_text.to_string() + "\n" + prettier_format_suffix,
 796        "Prettier formatting was not applied to client buffer after client's request"
 797    );
 798
 799    // User A opens and formats the same buffer too
 800    let buffer_a = project_a
 801        .update(cx_a, |p, cx| {
 802            p.open_buffer((worktree_id, rel_path("a.ts")), cx)
 803        })
 804        .await
 805        .expect("user A opens buffer for formatting");
 806
 807    cx_a.update(|cx| {
 808        SettingsStore::update_global(cx, |store, cx| {
 809            store.update_user_settings(cx, |file| {
 810                file.project.all_languages.defaults.formatter = Some(FormatterList::default());
 811                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
 812                    allowed: Some(true),
 813                    ..Default::default()
 814                });
 815            });
 816        });
 817    });
 818    project_a
 819        .update(cx_a, |project, cx| {
 820            project.format(
 821                HashSet::from_iter([buffer_a.clone()]),
 822                LspFormatTarget::Buffers,
 823                true,
 824                FormatTrigger::Manual,
 825                cx,
 826            )
 827        })
 828        .await
 829        .unwrap();
 830
 831    executor.run_until_parked();
 832    assert_eq!(
 833        buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
 834        buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
 835        "Prettier formatting was not applied to client buffer after host's request"
 836    );
 837}
 838
 839#[gpui::test]
 840async fn test_remote_server_debugger(
 841    cx_a: &mut TestAppContext,
 842    server_cx: &mut TestAppContext,
 843    executor: BackgroundExecutor,
 844) {
 845    cx_a.update(|cx| {
 846        release_channel::init(semver::Version::new(0, 0, 0), cx);
 847        command_palette_hooks::init(cx);
 848        zlog::init_test();
 849        dap_adapters::init(cx);
 850    });
 851    server_cx.update(|cx| {
 852        release_channel::init(semver::Version::new(0, 0, 0), cx);
 853        dap_adapters::init(cx);
 854    });
 855    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 856    let remote_fs = FakeFs::new(server_cx.executor());
 857    remote_fs
 858        .insert_tree(
 859            path!("/code"),
 860            json!({
 861                "lib.rs": "fn one() -> usize { 1 }"
 862            }),
 863        )
 864        .await;
 865
 866    // User A connects to the remote project via SSH.
 867    server_cx.update(HeadlessProject::init);
 868    let remote_http_client = Arc::new(BlockedHttpClient);
 869    let node = NodeRuntime::unavailable();
 870    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 871    let _headless_project = server_cx.new(|cx| {
 872        HeadlessProject::new(
 873            HeadlessAppState {
 874                session: server_ssh,
 875                fs: remote_fs.clone(),
 876                http_client: remote_http_client,
 877                node_runtime: node,
 878                languages,
 879                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 880                startup_time: std::time::Instant::now(),
 881            },
 882            false,
 883            cx,
 884        )
 885    });
 886
 887    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 888    let mut server = TestServer::start(server_cx.executor()).await;
 889    let client_a = server.create_client(cx_a, "user_a").await;
 890    cx_a.update(|cx| {
 891        debugger_ui::init(cx);
 892        command_palette_hooks::init(cx);
 893    });
 894    let (project_a, _) = client_a
 895        .build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
 896        .await;
 897
 898    let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
 899
 900    let debugger_panel = workspace
 901        .update_in(cx_a, |_workspace, window, cx| {
 902            cx.spawn_in(window, DebugPanel::load)
 903        })
 904        .await
 905        .unwrap();
 906
 907    workspace.update_in(cx_a, |workspace, window, cx| {
 908        workspace.add_panel(debugger_panel, window, cx);
 909    });
 910
 911    cx_a.run_until_parked();
 912    let debug_panel = workspace
 913        .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
 914        .unwrap();
 915
 916    let workspace_window = cx_a
 917        .window_handle()
 918        .downcast::<workspace::MultiWorkspace>()
 919        .unwrap();
 920
 921    let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
 922    cx_a.run_until_parked();
 923    debug_panel.update(cx_a, |debug_panel, cx| {
 924        assert_eq!(
 925            debug_panel.active_session().unwrap().read(cx).session(cx),
 926            session.clone()
 927        )
 928    });
 929
 930    session.update(
 931        cx_a,
 932        |session: &mut project::debugger::session::Session, _| {
 933            assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
 934        },
 935    );
 936
 937    let shutdown_session = workspace.update(cx_a, |workspace, cx| {
 938        workspace.project().update(cx, |project, cx| {
 939            project.dap_store().update(cx, |dap_store, cx| {
 940                dap_store.shutdown_session(session.read(cx).session_id(), cx)
 941            })
 942        })
 943    });
 944
 945    client_ssh.update(cx_a, |a, _| {
 946        a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
 947    });
 948
 949    shutdown_session.await.unwrap();
 950}
 951
 952#[gpui::test]
 953async fn test_slow_adapter_startup_retries(
 954    cx_a: &mut TestAppContext,
 955    server_cx: &mut TestAppContext,
 956    executor: BackgroundExecutor,
 957) {
 958    cx_a.update(|cx| {
 959        release_channel::init(semver::Version::new(0, 0, 0), cx);
 960        command_palette_hooks::init(cx);
 961        zlog::init_test();
 962        dap_adapters::init(cx);
 963    });
 964    server_cx.update(|cx| {
 965        release_channel::init(semver::Version::new(0, 0, 0), cx);
 966        dap_adapters::init(cx);
 967    });
 968    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 969    let remote_fs = FakeFs::new(server_cx.executor());
 970    remote_fs
 971        .insert_tree(
 972            path!("/code"),
 973            json!({
 974                "lib.rs": "fn one() -> usize { 1 }"
 975            }),
 976        )
 977        .await;
 978
 979    // User A connects to the remote project via SSH.
 980    server_cx.update(HeadlessProject::init);
 981    let remote_http_client = Arc::new(BlockedHttpClient);
 982    let node = NodeRuntime::unavailable();
 983    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 984    let _headless_project = server_cx.new(|cx| {
 985        HeadlessProject::new(
 986            HeadlessAppState {
 987                session: server_ssh,
 988                fs: remote_fs.clone(),
 989                http_client: remote_http_client,
 990                node_runtime: node,
 991                languages,
 992                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 993                startup_time: std::time::Instant::now(),
 994            },
 995            false,
 996            cx,
 997        )
 998    });
 999
1000    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
1001    let mut server = TestServer::start(server_cx.executor()).await;
1002    let client_a = server.create_client(cx_a, "user_a").await;
1003    cx_a.update(|cx| {
1004        debugger_ui::init(cx);
1005        command_palette_hooks::init(cx);
1006    });
1007    let (project_a, _) = client_a
1008        .build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
1009        .await;
1010
1011    let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
1012
1013    let debugger_panel = workspace
1014        .update_in(cx_a, |_workspace, window, cx| {
1015            cx.spawn_in(window, DebugPanel::load)
1016        })
1017        .await
1018        .unwrap();
1019
1020    workspace.update_in(cx_a, |workspace, window, cx| {
1021        workspace.add_panel(debugger_panel, window, cx);
1022    });
1023
1024    cx_a.run_until_parked();
1025    let debug_panel = workspace
1026        .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
1027        .unwrap();
1028
1029    let workspace_window = cx_a
1030        .window_handle()
1031        .downcast::<workspace::MultiWorkspace>()
1032        .unwrap();
1033
1034    let count = Arc::new(AtomicUsize::new(0));
1035    let session = debugger_ui::tests::start_debug_session_with(
1036        &workspace_window,
1037        cx_a,
1038        DebugTaskDefinition {
1039            adapter: "fake-adapter".into(),
1040            label: "test".into(),
1041            config: json!({
1042                "request": "launch"
1043            }),
1044            tcp_connection: Some(TcpArgumentsTemplate {
1045                port: None,
1046                host: None,
1047                timeout: None,
1048            }),
1049        },
1050        move |client| {
1051            let count = count.clone();
1052            client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
1053                if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
1054                    return RequestHandling::Exit;
1055                }
1056                RequestHandling::Respond(Ok(Capabilities::default()))
1057            });
1058        },
1059    )
1060    .unwrap();
1061    cx_a.run_until_parked();
1062
1063    let client = session.update(
1064        cx_a,
1065        |session: &mut project::debugger::session::Session, _| session.adapter_client().unwrap(),
1066    );
1067    client
1068        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1069            reason: dap::StoppedEventReason::Pause,
1070            description: None,
1071            thread_id: Some(1),
1072            preserve_focus_hint: None,
1073            text: None,
1074            all_threads_stopped: None,
1075            hit_breakpoint_ids: None,
1076        }))
1077        .await;
1078
1079    cx_a.run_until_parked();
1080
1081    let active_session = debug_panel
1082        .update(cx_a, |this, _| this.active_session())
1083        .unwrap();
1084
1085    let running_state = active_session.update(cx_a, |active_session, _| {
1086        active_session.running_state().clone()
1087    });
1088
1089    assert_eq!(
1090        client.id(),
1091        running_state.read_with(cx_a, |running_state, _| running_state.session_id())
1092    );
1093    assert_eq!(
1094        ThreadId(1),
1095        running_state.read_with(cx_a, |running_state, _| running_state
1096            .selected_thread_id()
1097            .unwrap())
1098    );
1099
1100    let shutdown_session = workspace.update(cx_a, |workspace, cx| {
1101        workspace.project().update(cx, |project, cx| {
1102            project.dap_store().update(cx, |dap_store, cx| {
1103                dap_store.shutdown_session(session.read(cx).session_id(), cx)
1104            })
1105        })
1106    });
1107
1108    client_ssh.update(cx_a, |a, _| {
1109        a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
1110    });
1111
1112    shutdown_session.await.unwrap();
1113}
1114
1115#[gpui::test]
1116async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
1117    cx_a.update(|cx| {
1118        release_channel::init(semver::Version::new(0, 0, 0), cx);
1119        project::trusted_worktrees::init(HashMap::default(), cx);
1120    });
1121    server_cx.update(|cx| {
1122        release_channel::init(semver::Version::new(0, 0, 0), cx);
1123        project::trusted_worktrees::init(HashMap::default(), cx);
1124    });
1125
1126    let mut server = TestServer::start(cx_a.executor().clone()).await;
1127    let client_a = server.create_client(cx_a, "user_a").await;
1128
1129    let server_name = "override-rust-analyzer";
1130    let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
1131
1132    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
1133    let remote_fs = FakeFs::new(server_cx.executor());
1134    remote_fs
1135        .insert_tree(
1136            path!("/projects"),
1137            json!({
1138                "project_a": {
1139                    ".zed": {
1140                        "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
1141                    },
1142                    "main.rs": "fn main() {}"
1143                },
1144                "project_b": { "lib.rs": "pub fn lib() {}" }
1145            }),
1146        )
1147        .await;
1148
1149    server_cx.update(HeadlessProject::init);
1150    let remote_http_client = Arc::new(BlockedHttpClient);
1151    let node = NodeRuntime::unavailable();
1152    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
1153    languages.add(rust_lang());
1154
1155    let capabilities = lsp::ServerCapabilities {
1156        inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1157        ..lsp::ServerCapabilities::default()
1158    };
1159    let mut fake_language_servers = languages.register_fake_lsp(
1160        "Rust",
1161        FakeLspAdapter {
1162            name: server_name,
1163            capabilities: capabilities.clone(),
1164            initializer: Some(Box::new({
1165                let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
1166                move |fake_server| {
1167                    let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
1168                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1169                        move |_params, _| {
1170                            lsp_inlay_hint_request_count.fetch_add(1, Ordering::Release);
1171                            async move {
1172                                Ok(Some(vec![lsp::InlayHint {
1173                                    position: lsp::Position::new(0, 0),
1174                                    label: lsp::InlayHintLabel::String("hint".to_string()),
1175                                    kind: None,
1176                                    text_edits: None,
1177                                    tooltip: None,
1178                                    padding_left: None,
1179                                    padding_right: None,
1180                                    data: None,
1181                                }]))
1182                            }
1183                        },
1184                    );
1185                }
1186            })),
1187            ..FakeLspAdapter::default()
1188        },
1189    );
1190
1191    let _headless_project = server_cx.new(|cx| {
1192        HeadlessProject::new(
1193            HeadlessAppState {
1194                session: server_ssh,
1195                fs: remote_fs.clone(),
1196                http_client: remote_http_client,
1197                node_runtime: node,
1198                languages,
1199                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
1200                startup_time: std::time::Instant::now(),
1201            },
1202            true,
1203            cx,
1204        )
1205    });
1206
1207    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
1208    let (project_a, worktree_id_a) = client_a
1209        .build_ssh_project(path!("/projects/project_a"), client_ssh.clone(), true, cx_a)
1210        .await;
1211
1212    cx_a.update(|cx| {
1213        release_channel::init(semver::Version::new(0, 0, 0), cx);
1214
1215        SettingsStore::update_global(cx, |store, cx| {
1216            store.update_user_settings(cx, |settings| {
1217                let language_settings = &mut settings.project.all_languages.defaults;
1218                language_settings.inlay_hints = Some(InlayHintSettingsContent {
1219                    enabled: Some(true),
1220                    ..InlayHintSettingsContent::default()
1221                })
1222            });
1223        });
1224    });
1225
1226    project_a
1227        .update(cx_a, |project, cx| {
1228            project.languages().add(rust_lang());
1229            project.languages().register_fake_lsp_adapter(
1230                "Rust",
1231                FakeLspAdapter {
1232                    name: server_name,
1233                    capabilities,
1234                    ..FakeLspAdapter::default()
1235                },
1236            );
1237            project.find_or_create_worktree(path!("/projects/project_b"), true, cx)
1238        })
1239        .await
1240        .unwrap();
1241
1242    cx_a.run_until_parked();
1243
1244    let worktree_ids = project_a.read_with(cx_a, |project, cx| {
1245        project
1246            .worktrees(cx)
1247            .map(|wt| wt.read(cx).id())
1248            .collect::<Vec<_>>()
1249    });
1250    assert_eq!(worktree_ids.len(), 2);
1251
1252    let trusted_worktrees =
1253        cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
1254    let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
1255
1256    let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1257        store.can_trust(&worktree_store, worktree_ids[0], cx)
1258    });
1259    let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1260        store.can_trust(&worktree_store, worktree_ids[1], cx)
1261    });
1262    assert!(!can_trust_a, "project_a should be restricted initially");
1263    assert!(!can_trust_b, "project_b should be restricted initially");
1264
1265    let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
1266        store.has_restricted_worktrees(&worktree_store, cx)
1267    });
1268    assert!(has_restricted, "should have restricted worktrees");
1269
1270    let buffer_before_approval = project_a
1271        .update(cx_a, |project, cx| {
1272            project.open_buffer((worktree_id_a, rel_path("main.rs")), cx)
1273        })
1274        .await
1275        .unwrap();
1276
1277    let (editor, cx_a) = cx_a.add_window_view(|window, cx| {
1278        Editor::new(
1279            EditorMode::full(),
1280            cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
1281            Some(project_a.clone()),
1282            window,
1283            cx,
1284        )
1285    });
1286    cx_a.run_until_parked();
1287    let fake_language_server = fake_language_servers.next();
1288
1289    cx_a.read(|cx| {
1290        assert_eq!(
1291            LanguageSettings::for_buffer(buffer_before_approval.read(cx), cx).language_servers,
1292            ["...".to_string()],
1293            "remote .zed/settings.json must not sync before trust approval"
1294        )
1295    });
1296
1297    editor.update_in(cx_a, |editor, window, cx| {
1298        editor.handle_input("1", window, cx);
1299    });
1300    cx_a.run_until_parked();
1301    cx_a.executor().advance_clock(Duration::from_secs(1));
1302    assert_eq!(
1303        lsp_inlay_hint_request_count.load(Ordering::Acquire),
1304        0,
1305        "inlay hints must not be queried before trust approval"
1306    );
1307
1308    trusted_worktrees.update(cx_a, |store, cx| {
1309        store.trust(
1310            &worktree_store,
1311            HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
1312            cx,
1313        );
1314    });
1315    cx_a.run_until_parked();
1316
1317    cx_a.read(|cx| {
1318        assert_eq!(
1319            LanguageSettings::for_buffer(buffer_before_approval.read(cx), cx).language_servers,
1320            ["override-rust-analyzer".to_string()],
1321            "remote .zed/settings.json should sync after trust approval"
1322        )
1323    });
1324    let _fake_language_server = fake_language_server.await.unwrap();
1325    editor.update_in(cx_a, |editor, window, cx| {
1326        editor.handle_input("1", window, cx);
1327    });
1328    cx_a.run_until_parked();
1329    cx_a.executor().advance_clock(Duration::from_secs(1));
1330    assert!(
1331        lsp_inlay_hint_request_count.load(Ordering::Acquire) > 0,
1332        "inlay hints should be queried after trust approval"
1333    );
1334
1335    let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1336        store.can_trust(&worktree_store, worktree_ids[0], cx)
1337    });
1338    let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1339        store.can_trust(&worktree_store, worktree_ids[1], cx)
1340    });
1341    assert!(can_trust_a, "project_a should be trusted after trust()");
1342    assert!(!can_trust_b, "project_b should still be restricted");
1343
1344    trusted_worktrees.update(cx_a, |store, cx| {
1345        store.trust(
1346            &worktree_store,
1347            HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1348            cx,
1349        );
1350    });
1351
1352    let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1353        store.can_trust(&worktree_store, worktree_ids[0], cx)
1354    });
1355    let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1356        store.can_trust(&worktree_store, worktree_ids[1], cx)
1357    });
1358    assert!(can_trust_a, "project_a should remain trusted");
1359    assert!(can_trust_b, "project_b should now be trusted");
1360
1361    let has_restricted_after = trusted_worktrees.read_with(cx_a, |store, cx| {
1362        store.has_restricted_worktrees(&worktree_store, cx)
1363    });
1364    assert!(
1365        !has_restricted_after,
1366        "should have no restricted worktrees after trusting both"
1367    );
1368}