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