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