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("/project");
 473    cx_b.update(|cx| {
 474        repo_b.update(cx, |repo, _| {
 475            repo.create_worktree(
 476                "feature-branch".to_string(),
 477                worktree_directory.clone(),
 478                Some("abc123".to_string()),
 479            )
 480        })
 481    })
 482    .await
 483    .unwrap()
 484    .unwrap();
 485
 486    executor.run_until_parked();
 487
 488    let worktrees = cx_b
 489        .update(|cx| repo_b.update(cx, |repo, _| repo.worktrees()))
 490        .await
 491        .unwrap()
 492        .unwrap();
 493    assert_eq!(worktrees.len(), 2);
 494    assert_eq!(worktrees[1].path, worktree_directory.join("feature-branch"));
 495    assert_eq!(worktrees[1].ref_name.as_ref(), "refs/heads/feature-branch");
 496    assert_eq!(worktrees[1].sha.as_ref(), "abc123");
 497
 498    let server_worktrees = {
 499        let server_repo = server_cx.update(|cx| {
 500            headless_project.update(cx, |headless_project, cx| {
 501                headless_project
 502                    .git_store
 503                    .read(cx)
 504                    .repositories()
 505                    .values()
 506                    .next()
 507                    .unwrap()
 508                    .clone()
 509            })
 510        });
 511        server_cx
 512            .update(|cx| server_repo.update(cx, |repo, _| repo.worktrees()))
 513            .await
 514            .unwrap()
 515            .unwrap()
 516    };
 517    assert_eq!(server_worktrees.len(), 2);
 518    assert_eq!(
 519        server_worktrees[1].path,
 520        worktree_directory.join("feature-branch")
 521    );
 522
 523    // Host (client A) renames the worktree via SSH
 524    let repo_a = cx_a.update(|cx| {
 525        project_a
 526            .read(cx)
 527            .repositories(cx)
 528            .values()
 529            .next()
 530            .unwrap()
 531            .clone()
 532    });
 533    cx_a.update(|cx| {
 534        repo_a.update(cx, |repository, _| {
 535            repository.rename_worktree(
 536                PathBuf::from("/project/feature-branch"),
 537                PathBuf::from("/project/renamed-branch"),
 538            )
 539        })
 540    })
 541    .await
 542    .unwrap()
 543    .unwrap();
 544
 545    executor.run_until_parked();
 546
 547    let host_worktrees = cx_a
 548        .update(|cx| repo_a.update(cx, |repository, _| repository.worktrees()))
 549        .await
 550        .unwrap()
 551        .unwrap();
 552    assert_eq!(
 553        host_worktrees.len(),
 554        2,
 555        "Host should still have 2 worktrees after rename"
 556    );
 557    assert_eq!(
 558        host_worktrees[1].path,
 559        PathBuf::from("/project/renamed-branch")
 560    );
 561
 562    let server_worktrees = {
 563        let server_repo = server_cx.update(|cx| {
 564            headless_project.update(cx, |headless_project, cx| {
 565                headless_project
 566                    .git_store
 567                    .read(cx)
 568                    .repositories()
 569                    .values()
 570                    .next()
 571                    .unwrap()
 572                    .clone()
 573            })
 574        });
 575        server_cx
 576            .update(|cx| server_repo.update(cx, |repo, _| repo.worktrees()))
 577            .await
 578            .unwrap()
 579            .unwrap()
 580    };
 581    assert_eq!(
 582        server_worktrees.len(),
 583        2,
 584        "Server should still have 2 worktrees after rename"
 585    );
 586    assert_eq!(
 587        server_worktrees[1].path,
 588        PathBuf::from("/project/renamed-branch")
 589    );
 590
 591    // Host (client A) removes the renamed worktree via SSH
 592    cx_a.update(|cx| {
 593        repo_a.update(cx, |repository, _| {
 594            repository.remove_worktree(PathBuf::from("/project/renamed-branch"), false)
 595        })
 596    })
 597    .await
 598    .unwrap()
 599    .unwrap();
 600
 601    executor.run_until_parked();
 602
 603    let host_worktrees = cx_a
 604        .update(|cx| repo_a.update(cx, |repository, _| repository.worktrees()))
 605        .await
 606        .unwrap()
 607        .unwrap();
 608    assert_eq!(
 609        host_worktrees.len(),
 610        1,
 611        "Host should only have the main worktree after removal"
 612    );
 613
 614    let server_worktrees = {
 615        let server_repo = server_cx.update(|cx| {
 616            headless_project.update(cx, |headless_project, cx| {
 617                headless_project
 618                    .git_store
 619                    .read(cx)
 620                    .repositories()
 621                    .values()
 622                    .next()
 623                    .unwrap()
 624                    .clone()
 625            })
 626        });
 627        server_cx
 628            .update(|cx| server_repo.update(cx, |repo, _| repo.worktrees()))
 629            .await
 630            .unwrap()
 631            .unwrap()
 632    };
 633    assert_eq!(
 634        server_worktrees.len(),
 635        1,
 636        "Server should only have the main worktree after removal"
 637    );
 638}
 639
 640#[gpui::test]
 641async fn test_ssh_collaboration_formatting_with_prettier(
 642    executor: BackgroundExecutor,
 643    cx_a: &mut TestAppContext,
 644    cx_b: &mut TestAppContext,
 645    server_cx: &mut TestAppContext,
 646) {
 647    cx_a.set_name("a");
 648    cx_b.set_name("b");
 649    server_cx.set_name("server");
 650
 651    cx_a.update(|cx| {
 652        release_channel::init(semver::Version::new(0, 0, 0), cx);
 653    });
 654    server_cx.update(|cx| {
 655        release_channel::init(semver::Version::new(0, 0, 0), cx);
 656    });
 657
 658    let mut server = TestServer::start(executor.clone()).await;
 659    let client_a = server.create_client(cx_a, "user_a").await;
 660    let client_b = server.create_client(cx_b, "user_b").await;
 661    server
 662        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 663        .await;
 664
 665    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 666    let remote_fs = FakeFs::new(server_cx.executor());
 667    let buffer_text = "let one = \"two\"";
 668    let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
 669    remote_fs
 670        .insert_tree(
 671            path!("/project"),
 672            serde_json::json!({ "a.ts": buffer_text }),
 673        )
 674        .await;
 675
 676    let test_plugin = "test_plugin";
 677    let ts_lang = Arc::new(Language::new(
 678        LanguageConfig {
 679            name: "TypeScript".into(),
 680            matcher: LanguageMatcher {
 681                path_suffixes: vec!["ts".to_string()],
 682                ..LanguageMatcher::default()
 683            },
 684            ..LanguageConfig::default()
 685        },
 686        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
 687    ));
 688    client_a.language_registry().add(ts_lang.clone());
 689    client_b.language_registry().add(ts_lang.clone());
 690
 691    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 692    let mut fake_language_servers = languages.register_fake_lsp(
 693        "TypeScript",
 694        FakeLspAdapter {
 695            prettier_plugins: vec![test_plugin],
 696            ..Default::default()
 697        },
 698    );
 699
 700    // User A connects to the remote project via SSH.
 701    server_cx.update(HeadlessProject::init);
 702    let remote_http_client = Arc::new(BlockedHttpClient);
 703    let _headless_project = server_cx.new(|cx| {
 704        HeadlessProject::new(
 705            HeadlessAppState {
 706                session: server_ssh,
 707                fs: remote_fs.clone(),
 708                http_client: remote_http_client,
 709                node_runtime: NodeRuntime::unavailable(),
 710                languages,
 711                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 712                startup_time: std::time::Instant::now(),
 713            },
 714            false,
 715            cx,
 716        )
 717    });
 718
 719    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 720    let (project_a, worktree_id) = client_a
 721        .build_ssh_project(path!("/project"), client_ssh, false, cx_a)
 722        .await;
 723
 724    // While the SSH worktree is being scanned, user A shares the remote project.
 725    let active_call_a = cx_a.read(ActiveCall::global);
 726    let project_id = active_call_a
 727        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 728        .await
 729        .unwrap();
 730
 731    // User B joins the project.
 732    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 733    executor.run_until_parked();
 734
 735    // Opens the buffer and formats it
 736    let (buffer_b, _handle) = project_b
 737        .update(cx_b, |p, cx| {
 738            p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
 739        })
 740        .await
 741        .expect("user B opens buffer for formatting");
 742
 743    cx_a.update(|cx| {
 744        SettingsStore::update_global(cx, |store, cx| {
 745            store.update_user_settings(cx, |file| {
 746                file.project.all_languages.defaults.formatter = Some(FormatterList::default());
 747                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
 748                    allowed: Some(true),
 749                    ..Default::default()
 750                });
 751            });
 752        });
 753    });
 754    cx_b.update(|cx| {
 755        SettingsStore::update_global(cx, |store, cx| {
 756            store.update_user_settings(cx, |file| {
 757                file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
 758                    Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
 759                ));
 760                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
 761                    allowed: Some(true),
 762                    ..Default::default()
 763                });
 764            });
 765        });
 766    });
 767    let fake_language_server = fake_language_servers.next().await.unwrap();
 768    fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
 769        panic!(
 770            "Unexpected: prettier should be preferred since it's enabled and language supports it"
 771        )
 772    });
 773
 774    project_b
 775        .update(cx_b, |project, cx| {
 776            project.format(
 777                HashSet::from_iter([buffer_b.clone()]),
 778                LspFormatTarget::Buffers,
 779                true,
 780                FormatTrigger::Save,
 781                cx,
 782            )
 783        })
 784        .await
 785        .unwrap();
 786
 787    executor.run_until_parked();
 788    assert_eq!(
 789        buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
 790        buffer_text.to_string() + "\n" + prettier_format_suffix,
 791        "Prettier formatting was not applied to client buffer after client's request"
 792    );
 793
 794    // User A opens and formats the same buffer too
 795    let buffer_a = project_a
 796        .update(cx_a, |p, cx| {
 797            p.open_buffer((worktree_id, rel_path("a.ts")), cx)
 798        })
 799        .await
 800        .expect("user A opens buffer for formatting");
 801
 802    cx_a.update(|cx| {
 803        SettingsStore::update_global(cx, |store, cx| {
 804            store.update_user_settings(cx, |file| {
 805                file.project.all_languages.defaults.formatter = Some(FormatterList::default());
 806                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
 807                    allowed: Some(true),
 808                    ..Default::default()
 809                });
 810            });
 811        });
 812    });
 813    project_a
 814        .update(cx_a, |project, cx| {
 815            project.format(
 816                HashSet::from_iter([buffer_a.clone()]),
 817                LspFormatTarget::Buffers,
 818                true,
 819                FormatTrigger::Manual,
 820                cx,
 821            )
 822        })
 823        .await
 824        .unwrap();
 825
 826    executor.run_until_parked();
 827    assert_eq!(
 828        buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
 829        buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
 830        "Prettier formatting was not applied to client buffer after host's request"
 831    );
 832}
 833
 834#[gpui::test]
 835async fn test_remote_server_debugger(
 836    cx_a: &mut TestAppContext,
 837    server_cx: &mut TestAppContext,
 838    executor: BackgroundExecutor,
 839) {
 840    cx_a.update(|cx| {
 841        release_channel::init(semver::Version::new(0, 0, 0), cx);
 842        command_palette_hooks::init(cx);
 843        zlog::init_test();
 844        dap_adapters::init(cx);
 845    });
 846    server_cx.update(|cx| {
 847        release_channel::init(semver::Version::new(0, 0, 0), cx);
 848        dap_adapters::init(cx);
 849    });
 850    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 851    let remote_fs = FakeFs::new(server_cx.executor());
 852    remote_fs
 853        .insert_tree(
 854            path!("/code"),
 855            json!({
 856                "lib.rs": "fn one() -> usize { 1 }"
 857            }),
 858        )
 859        .await;
 860
 861    // User A connects to the remote project via SSH.
 862    server_cx.update(HeadlessProject::init);
 863    let remote_http_client = Arc::new(BlockedHttpClient);
 864    let node = NodeRuntime::unavailable();
 865    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 866    let _headless_project = server_cx.new(|cx| {
 867        HeadlessProject::new(
 868            HeadlessAppState {
 869                session: server_ssh,
 870                fs: remote_fs.clone(),
 871                http_client: remote_http_client,
 872                node_runtime: node,
 873                languages,
 874                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 875                startup_time: std::time::Instant::now(),
 876            },
 877            false,
 878            cx,
 879        )
 880    });
 881
 882    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 883    let mut server = TestServer::start(server_cx.executor()).await;
 884    let client_a = server.create_client(cx_a, "user_a").await;
 885    cx_a.update(|cx| {
 886        debugger_ui::init(cx);
 887        command_palette_hooks::init(cx);
 888    });
 889    let (project_a, _) = client_a
 890        .build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
 891        .await;
 892
 893    let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
 894
 895    let debugger_panel = workspace
 896        .update_in(cx_a, |_workspace, window, cx| {
 897            cx.spawn_in(window, DebugPanel::load)
 898        })
 899        .await
 900        .unwrap();
 901
 902    workspace.update_in(cx_a, |workspace, window, cx| {
 903        workspace.add_panel(debugger_panel, window, cx);
 904    });
 905
 906    cx_a.run_until_parked();
 907    let debug_panel = workspace
 908        .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
 909        .unwrap();
 910
 911    let workspace_window = cx_a
 912        .window_handle()
 913        .downcast::<workspace::MultiWorkspace>()
 914        .unwrap();
 915
 916    let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
 917    cx_a.run_until_parked();
 918    debug_panel.update(cx_a, |debug_panel, cx| {
 919        assert_eq!(
 920            debug_panel.active_session().unwrap().read(cx).session(cx),
 921            session.clone()
 922        )
 923    });
 924
 925    session.update(
 926        cx_a,
 927        |session: &mut project::debugger::session::Session, _| {
 928            assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
 929        },
 930    );
 931
 932    let shutdown_session = workspace.update(cx_a, |workspace, cx| {
 933        workspace.project().update(cx, |project, cx| {
 934            project.dap_store().update(cx, |dap_store, cx| {
 935                dap_store.shutdown_session(session.read(cx).session_id(), cx)
 936            })
 937        })
 938    });
 939
 940    client_ssh.update(cx_a, |a, _| {
 941        a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
 942    });
 943
 944    shutdown_session.await.unwrap();
 945}
 946
 947#[gpui::test]
 948async fn test_slow_adapter_startup_retries(
 949    cx_a: &mut TestAppContext,
 950    server_cx: &mut TestAppContext,
 951    executor: BackgroundExecutor,
 952) {
 953    cx_a.update(|cx| {
 954        release_channel::init(semver::Version::new(0, 0, 0), cx);
 955        command_palette_hooks::init(cx);
 956        zlog::init_test();
 957        dap_adapters::init(cx);
 958    });
 959    server_cx.update(|cx| {
 960        release_channel::init(semver::Version::new(0, 0, 0), cx);
 961        dap_adapters::init(cx);
 962    });
 963    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 964    let remote_fs = FakeFs::new(server_cx.executor());
 965    remote_fs
 966        .insert_tree(
 967            path!("/code"),
 968            json!({
 969                "lib.rs": "fn one() -> usize { 1 }"
 970            }),
 971        )
 972        .await;
 973
 974    // User A connects to the remote project via SSH.
 975    server_cx.update(HeadlessProject::init);
 976    let remote_http_client = Arc::new(BlockedHttpClient);
 977    let node = NodeRuntime::unavailable();
 978    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 979    let _headless_project = server_cx.new(|cx| {
 980        HeadlessProject::new(
 981            HeadlessAppState {
 982                session: server_ssh,
 983                fs: remote_fs.clone(),
 984                http_client: remote_http_client,
 985                node_runtime: node,
 986                languages,
 987                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 988                startup_time: std::time::Instant::now(),
 989            },
 990            false,
 991            cx,
 992        )
 993    });
 994
 995    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 996    let mut server = TestServer::start(server_cx.executor()).await;
 997    let client_a = server.create_client(cx_a, "user_a").await;
 998    cx_a.update(|cx| {
 999        debugger_ui::init(cx);
1000        command_palette_hooks::init(cx);
1001    });
1002    let (project_a, _) = client_a
1003        .build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
1004        .await;
1005
1006    let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
1007
1008    let debugger_panel = workspace
1009        .update_in(cx_a, |_workspace, window, cx| {
1010            cx.spawn_in(window, DebugPanel::load)
1011        })
1012        .await
1013        .unwrap();
1014
1015    workspace.update_in(cx_a, |workspace, window, cx| {
1016        workspace.add_panel(debugger_panel, window, cx);
1017    });
1018
1019    cx_a.run_until_parked();
1020    let debug_panel = workspace
1021        .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
1022        .unwrap();
1023
1024    let workspace_window = cx_a
1025        .window_handle()
1026        .downcast::<workspace::MultiWorkspace>()
1027        .unwrap();
1028
1029    let count = Arc::new(AtomicUsize::new(0));
1030    let session = debugger_ui::tests::start_debug_session_with(
1031        &workspace_window,
1032        cx_a,
1033        DebugTaskDefinition {
1034            adapter: "fake-adapter".into(),
1035            label: "test".into(),
1036            config: json!({
1037                "request": "launch"
1038            }),
1039            tcp_connection: Some(TcpArgumentsTemplate {
1040                port: None,
1041                host: None,
1042                timeout: None,
1043            }),
1044        },
1045        move |client| {
1046            let count = count.clone();
1047            client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
1048                if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
1049                    return RequestHandling::Exit;
1050                }
1051                RequestHandling::Respond(Ok(Capabilities::default()))
1052            });
1053        },
1054    )
1055    .unwrap();
1056    cx_a.run_until_parked();
1057
1058    let client = session.update(
1059        cx_a,
1060        |session: &mut project::debugger::session::Session, _| session.adapter_client().unwrap(),
1061    );
1062    client
1063        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1064            reason: dap::StoppedEventReason::Pause,
1065            description: None,
1066            thread_id: Some(1),
1067            preserve_focus_hint: None,
1068            text: None,
1069            all_threads_stopped: None,
1070            hit_breakpoint_ids: None,
1071        }))
1072        .await;
1073
1074    cx_a.run_until_parked();
1075
1076    let active_session = debug_panel
1077        .update(cx_a, |this, _| this.active_session())
1078        .unwrap();
1079
1080    let running_state = active_session.update(cx_a, |active_session, _| {
1081        active_session.running_state().clone()
1082    });
1083
1084    assert_eq!(
1085        client.id(),
1086        running_state.read_with(cx_a, |running_state, _| running_state.session_id())
1087    );
1088    assert_eq!(
1089        ThreadId(1),
1090        running_state.read_with(cx_a, |running_state, _| running_state
1091            .selected_thread_id()
1092            .unwrap())
1093    );
1094
1095    let shutdown_session = workspace.update(cx_a, |workspace, cx| {
1096        workspace.project().update(cx, |project, cx| {
1097            project.dap_store().update(cx, |dap_store, cx| {
1098                dap_store.shutdown_session(session.read(cx).session_id(), cx)
1099            })
1100        })
1101    });
1102
1103    client_ssh.update(cx_a, |a, _| {
1104        a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
1105    });
1106
1107    shutdown_session.await.unwrap();
1108}
1109
1110#[gpui::test]
1111async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
1112    cx_a.update(|cx| {
1113        release_channel::init(semver::Version::new(0, 0, 0), cx);
1114        project::trusted_worktrees::init(HashMap::default(), cx);
1115    });
1116    server_cx.update(|cx| {
1117        release_channel::init(semver::Version::new(0, 0, 0), cx);
1118        project::trusted_worktrees::init(HashMap::default(), cx);
1119    });
1120
1121    let mut server = TestServer::start(cx_a.executor().clone()).await;
1122    let client_a = server.create_client(cx_a, "user_a").await;
1123
1124    let server_name = "override-rust-analyzer";
1125    let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
1126
1127    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
1128    let remote_fs = FakeFs::new(server_cx.executor());
1129    remote_fs
1130        .insert_tree(
1131            path!("/projects"),
1132            json!({
1133                "project_a": {
1134                    ".zed": {
1135                        "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
1136                    },
1137                    "main.rs": "fn main() {}"
1138                },
1139                "project_b": { "lib.rs": "pub fn lib() {}" }
1140            }),
1141        )
1142        .await;
1143
1144    server_cx.update(HeadlessProject::init);
1145    let remote_http_client = Arc::new(BlockedHttpClient);
1146    let node = NodeRuntime::unavailable();
1147    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
1148    languages.add(rust_lang());
1149
1150    let capabilities = lsp::ServerCapabilities {
1151        inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1152        ..lsp::ServerCapabilities::default()
1153    };
1154    let mut fake_language_servers = languages.register_fake_lsp(
1155        "Rust",
1156        FakeLspAdapter {
1157            name: server_name,
1158            capabilities: capabilities.clone(),
1159            initializer: Some(Box::new({
1160                let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
1161                move |fake_server| {
1162                    let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
1163                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1164                        move |_params, _| {
1165                            lsp_inlay_hint_request_count.fetch_add(1, Ordering::Release);
1166                            async move {
1167                                Ok(Some(vec![lsp::InlayHint {
1168                                    position: lsp::Position::new(0, 0),
1169                                    label: lsp::InlayHintLabel::String("hint".to_string()),
1170                                    kind: None,
1171                                    text_edits: None,
1172                                    tooltip: None,
1173                                    padding_left: None,
1174                                    padding_right: None,
1175                                    data: None,
1176                                }]))
1177                            }
1178                        },
1179                    );
1180                }
1181            })),
1182            ..FakeLspAdapter::default()
1183        },
1184    );
1185
1186    let _headless_project = server_cx.new(|cx| {
1187        HeadlessProject::new(
1188            HeadlessAppState {
1189                session: server_ssh,
1190                fs: remote_fs.clone(),
1191                http_client: remote_http_client,
1192                node_runtime: node,
1193                languages,
1194                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
1195                startup_time: std::time::Instant::now(),
1196            },
1197            true,
1198            cx,
1199        )
1200    });
1201
1202    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
1203    let (project_a, worktree_id_a) = client_a
1204        .build_ssh_project(path!("/projects/project_a"), client_ssh.clone(), true, cx_a)
1205        .await;
1206
1207    cx_a.update(|cx| {
1208        release_channel::init(semver::Version::new(0, 0, 0), cx);
1209
1210        SettingsStore::update_global(cx, |store, cx| {
1211            store.update_user_settings(cx, |settings| {
1212                let language_settings = &mut settings.project.all_languages.defaults;
1213                language_settings.inlay_hints = Some(InlayHintSettingsContent {
1214                    enabled: Some(true),
1215                    ..InlayHintSettingsContent::default()
1216                })
1217            });
1218        });
1219    });
1220
1221    project_a
1222        .update(cx_a, |project, cx| {
1223            project.languages().add(rust_lang());
1224            project.languages().register_fake_lsp_adapter(
1225                "Rust",
1226                FakeLspAdapter {
1227                    name: server_name,
1228                    capabilities,
1229                    ..FakeLspAdapter::default()
1230                },
1231            );
1232            project.find_or_create_worktree(path!("/projects/project_b"), true, cx)
1233        })
1234        .await
1235        .unwrap();
1236
1237    cx_a.run_until_parked();
1238
1239    let worktree_ids = project_a.read_with(cx_a, |project, cx| {
1240        project
1241            .worktrees(cx)
1242            .map(|wt| wt.read(cx).id())
1243            .collect::<Vec<_>>()
1244    });
1245    assert_eq!(worktree_ids.len(), 2);
1246
1247    let trusted_worktrees =
1248        cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
1249    let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
1250
1251    let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1252        store.can_trust(&worktree_store, worktree_ids[0], cx)
1253    });
1254    let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1255        store.can_trust(&worktree_store, worktree_ids[1], cx)
1256    });
1257    assert!(!can_trust_a, "project_a should be restricted initially");
1258    assert!(!can_trust_b, "project_b should be restricted initially");
1259
1260    let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
1261        store.has_restricted_worktrees(&worktree_store, cx)
1262    });
1263    assert!(has_restricted, "should have restricted worktrees");
1264
1265    let buffer_before_approval = project_a
1266        .update(cx_a, |project, cx| {
1267            project.open_buffer((worktree_id_a, rel_path("main.rs")), cx)
1268        })
1269        .await
1270        .unwrap();
1271
1272    let (editor, cx_a) = cx_a.add_window_view(|window, cx| {
1273        Editor::new(
1274            EditorMode::full(),
1275            cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
1276            Some(project_a.clone()),
1277            window,
1278            cx,
1279        )
1280    });
1281    cx_a.run_until_parked();
1282    let fake_language_server = fake_language_servers.next();
1283
1284    cx_a.read(|cx| {
1285        assert_eq!(
1286            LanguageSettings::for_buffer(buffer_before_approval.read(cx), cx).language_servers,
1287            ["...".to_string()],
1288            "remote .zed/settings.json must not sync before trust approval"
1289        )
1290    });
1291
1292    editor.update_in(cx_a, |editor, window, cx| {
1293        editor.handle_input("1", window, cx);
1294    });
1295    cx_a.run_until_parked();
1296    cx_a.executor().advance_clock(Duration::from_secs(1));
1297    assert_eq!(
1298        lsp_inlay_hint_request_count.load(Ordering::Acquire),
1299        0,
1300        "inlay hints must not be queried before trust approval"
1301    );
1302
1303    trusted_worktrees.update(cx_a, |store, cx| {
1304        store.trust(
1305            &worktree_store,
1306            HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
1307            cx,
1308        );
1309    });
1310    cx_a.run_until_parked();
1311
1312    cx_a.read(|cx| {
1313        assert_eq!(
1314            LanguageSettings::for_buffer(buffer_before_approval.read(cx), cx).language_servers,
1315            ["override-rust-analyzer".to_string()],
1316            "remote .zed/settings.json should sync after trust approval"
1317        )
1318    });
1319    let _fake_language_server = fake_language_server.await.unwrap();
1320    editor.update_in(cx_a, |editor, window, cx| {
1321        editor.handle_input("1", window, cx);
1322    });
1323    cx_a.run_until_parked();
1324    cx_a.executor().advance_clock(Duration::from_secs(1));
1325    assert!(
1326        lsp_inlay_hint_request_count.load(Ordering::Acquire) > 0,
1327        "inlay hints should be queried after trust approval"
1328    );
1329
1330    let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1331        store.can_trust(&worktree_store, worktree_ids[0], cx)
1332    });
1333    let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1334        store.can_trust(&worktree_store, worktree_ids[1], cx)
1335    });
1336    assert!(can_trust_a, "project_a should be trusted after trust()");
1337    assert!(!can_trust_b, "project_b should still be restricted");
1338
1339    trusted_worktrees.update(cx_a, |store, cx| {
1340        store.trust(
1341            &worktree_store,
1342            HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1343            cx,
1344        );
1345    });
1346
1347    let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1348        store.can_trust(&worktree_store, worktree_ids[0], cx)
1349    });
1350    let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1351        store.can_trust(&worktree_store, worktree_ids[1], cx)
1352    });
1353    assert!(can_trust_a, "project_a should remain trusted");
1354    assert!(can_trust_b, "project_b should now be trusted");
1355
1356    let has_restricted_after = trusted_worktrees.read_with(cx_a, |store, cx| {
1357        store.has_restricted_worktrees(&worktree_store, cx)
1358    });
1359    assert!(
1360        !has_restricted_after,
1361        "should have no restricted worktrees after trusting both"
1362    );
1363}