remote_editing_collaboration_tests.rs

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