remote_editing_collaboration_tests.rs

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