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