remote_editing_collaboration_tests.rs

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