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